protocol-proxy 2.8.1 → 2.8.3
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/lib/converters/anthropic-to-openai.js +14 -5
- package/lib/converters/openai-to-anthropic.js +4 -0
- package/lib/proxy-server.js +17 -3
- package/package.json +1 -1
- package/public/app.js +1972 -2027
- package/public/index.html +645 -440
- package/public/style.css +2112 -1996
- package/server.js +1838 -1129
package/public/app.js
CHANGED
|
@@ -1,2027 +1,1972 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
let
|
|
7
|
-
let
|
|
8
|
-
let
|
|
9
|
-
let
|
|
10
|
-
let
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
];
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
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
|
-
function
|
|
184
|
-
const
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
const
|
|
196
|
-
const
|
|
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
|
-
if (
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
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
|
-
const
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
const
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
} catch (err) {
|
|
871
|
-
showToast('
|
|
872
|
-
}
|
|
873
|
-
}
|
|
874
|
-
|
|
875
|
-
async function
|
|
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
|
-
if (
|
|
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
|
-
const
|
|
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
|
-
const
|
|
1095
|
-
const
|
|
1096
|
-
if
|
|
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
|
-
|
|
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
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
const
|
|
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
|
-
if (
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
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
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
function
|
|
1511
|
-
const
|
|
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
|
-
function
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
const
|
|
1540
|
-
const
|
|
1541
|
-
if (
|
|
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
|
-
|
|
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
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
const
|
|
1848
|
-
if (!
|
|
1849
|
-
|
|
1850
|
-
const
|
|
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
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
}
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
// ==================== 代理操作 ====================
|
|
1974
|
-
|
|
1975
|
-
async function startProxy(id) {
|
|
1976
|
-
try {
|
|
1977
|
-
await fetch(`/api/proxies/${id}/start`, { method: 'POST' });
|
|
1978
|
-
await loadProxies();
|
|
1979
|
-
} catch (err) {
|
|
1980
|
-
showToast('启动失败: ' + err.message, true);
|
|
1981
|
-
}
|
|
1982
|
-
}
|
|
1983
|
-
|
|
1984
|
-
async function stopProxy(id) {
|
|
1985
|
-
try {
|
|
1986
|
-
await fetch(`/api/proxies/${id}/stop`, { method: 'POST' });
|
|
1987
|
-
await loadProxies();
|
|
1988
|
-
} catch (err) {
|
|
1989
|
-
showToast('停止失败: ' + err.message, true);
|
|
1990
|
-
}
|
|
1991
|
-
}
|
|
1992
|
-
|
|
1993
|
-
async function deleteProxy(id) {
|
|
1994
|
-
const p = proxies.find(x => x.id === id);
|
|
1995
|
-
const ok = await showConfirm(`确定要删除代理配置 <strong>${escapeHtml(p?.name || '')}</strong> 吗?`);
|
|
1996
|
-
if (!ok) return;
|
|
1997
|
-
try {
|
|
1998
|
-
await fetch(`/api/proxies/${id}`, { method: 'DELETE' });
|
|
1999
|
-
await loadProxies();
|
|
2000
|
-
} catch (err) {
|
|
2001
|
-
showToast('删除失败: ' + err.message, true);
|
|
2002
|
-
}
|
|
2003
|
-
}
|
|
2004
|
-
|
|
2005
|
-
async function editProxy(id) {
|
|
2006
|
-
try {
|
|
2007
|
-
const res = await fetch(`/api/proxies/${id}`);
|
|
2008
|
-
if (!res.ok) throw new Error('加载失败');
|
|
2009
|
-
const full = await res.json();
|
|
2010
|
-
const idx = proxies.findIndex(p => p.id === id);
|
|
2011
|
-
if (idx !== -1) proxies[idx] = { ...proxies[idx], ...full };
|
|
2012
|
-
openModal(id);
|
|
2013
|
-
} catch (err) {
|
|
2014
|
-
showToast('加载代理配置失败: ' + err.message, true);
|
|
2015
|
-
}
|
|
2016
|
-
}
|
|
2017
|
-
|
|
2018
|
-
// ==================== 工具函数 ====================
|
|
2019
|
-
|
|
2020
|
-
function escapeHtml(text) {
|
|
2021
|
-
if (!text) return '';
|
|
2022
|
-
const div = document.createElement('div');
|
|
2023
|
-
div.textContent = text;
|
|
2024
|
-
return div.innerHTML;
|
|
2025
|
-
}
|
|
2026
|
-
|
|
2027
|
-
init();
|
|
1
|
+
// ================================================
|
|
2
|
+
// Protocol Proxy — App Logic
|
|
3
|
+
// ================================================
|
|
4
|
+
|
|
5
|
+
// ---------- State ----------
|
|
6
|
+
let proxies = [];
|
|
7
|
+
let providers = [];
|
|
8
|
+
let keyHealth = {};
|
|
9
|
+
let requestLogs = [];
|
|
10
|
+
let ws = null;
|
|
11
|
+
let statsRange = 'daily';
|
|
12
|
+
let statsProxyId = '';
|
|
13
|
+
let importData = null;
|
|
14
|
+
let editingProxyId = null;
|
|
15
|
+
let editingProviderId = null;
|
|
16
|
+
let currentPage = 'dashboard';
|
|
17
|
+
let providerPoolItems = [];
|
|
18
|
+
let providerModelTags = [];
|
|
19
|
+
let providerKeys = [];
|
|
20
|
+
let assistantMessages = [];
|
|
21
|
+
let assistantProxyId = '';
|
|
22
|
+
let assistantAbortController = null;
|
|
23
|
+
|
|
24
|
+
// ---------- Theme ----------
|
|
25
|
+
const THEMES = [
|
|
26
|
+
{ id: 'dark', icon: '\u263E', label: '\u6df1\u8272' },
|
|
27
|
+
{ id: 'light', icon: '\u2600', label: '\u6d45\u8272' },
|
|
28
|
+
{ id: 'midnight', icon: '\u2726', label: '\u5348\u591c\u7d2b' },
|
|
29
|
+
{ id: 'forest', icon: '\u2638', label: '\u68ee\u6797\u7eff' },
|
|
30
|
+
{ id: 'sunset', icon: '\u2605', label: '\u65e5\u843d\u6a59' },
|
|
31
|
+
{ id: 'ocean', icon: '\u265B', label: '\u6d77\u6d0b\u9752' },
|
|
32
|
+
{ id: 'sakura', icon: '\u273F', label: '\u6a31\u82b1\u7c89' },
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
function applyTheme(themeId) {
|
|
36
|
+
const t = THEMES.find(t => t.id === themeId) || THEMES[0];
|
|
37
|
+
document.documentElement.setAttribute('data-theme', t.id);
|
|
38
|
+
const icon = document.getElementById('theme-icon');
|
|
39
|
+
const label = document.getElementById('theme-label');
|
|
40
|
+
const select = document.getElementById('settings-theme');
|
|
41
|
+
if (icon) icon.textContent = t.icon;
|
|
42
|
+
if (label) label.textContent = t.label;
|
|
43
|
+
if (select) select.value = t.id;
|
|
44
|
+
localStorage.setItem('theme', t.id);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function cycleTheme() {
|
|
48
|
+
const current = document.documentElement.getAttribute('data-theme') || 'dark';
|
|
49
|
+
const idx = THEMES.findIndex(t => t.id === current);
|
|
50
|
+
const next = THEMES[(idx + 1) % THEMES.length];
|
|
51
|
+
applyTheme(next.id);
|
|
52
|
+
fetch('/api/settings', {
|
|
53
|
+
method: 'PUT',
|
|
54
|
+
headers: { 'Content-Type': 'application/json' },
|
|
55
|
+
body: JSON.stringify({ theme: next.id }),
|
|
56
|
+
}).catch(() => {});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
(async () => {
|
|
60
|
+
try {
|
|
61
|
+
const res = await fetch('/api/settings');
|
|
62
|
+
const settings = await res.json();
|
|
63
|
+
applyTheme(settings.theme || localStorage.getItem('theme') || 'dark');
|
|
64
|
+
} catch {
|
|
65
|
+
applyTheme(localStorage.getItem('theme') || 'dark');
|
|
66
|
+
}
|
|
67
|
+
})();
|
|
68
|
+
|
|
69
|
+
// ---------- Navigation ----------
|
|
70
|
+
function navigateTo(page) {
|
|
71
|
+
currentPage = page;
|
|
72
|
+
document.querySelectorAll('.page').forEach(p => p.classList.remove('active'));
|
|
73
|
+
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
|
|
74
|
+
const target = document.getElementById('page-' + page);
|
|
75
|
+
if (target) target.classList.add('active');
|
|
76
|
+
const nav = document.querySelector('.nav-item[data-page="' + page + '"]');
|
|
77
|
+
if (nav) nav.classList.add('active');
|
|
78
|
+
|
|
79
|
+
const titles = {
|
|
80
|
+
dashboard: '\u603b\u89c8',
|
|
81
|
+
proxies: '\u4ee3\u7406\u7ba1\u7406',
|
|
82
|
+
providers: '\u4f9b\u5e94\u5546\u7ba1\u7406',
|
|
83
|
+
stats: '\u7528\u91cf\u7edf\u8ba1',
|
|
84
|
+
'request-logs': '\u8bf7\u6c42\u65e5\u5fd7',
|
|
85
|
+
'system-logs': '\u7cfb\u7edf\u65e5\u5fd7',
|
|
86
|
+
assistant: '\u667a\u63a7\u52a9\u624b',
|
|
87
|
+
settings: '\u8bbe\u7f6e',
|
|
88
|
+
};
|
|
89
|
+
document.getElementById('page-title').textContent = titles[page] || page;
|
|
90
|
+
|
|
91
|
+
// Refresh data for specific pages
|
|
92
|
+
if (page === 'dashboard') refreshDashboard();
|
|
93
|
+
if (page === 'proxies') renderProxies();
|
|
94
|
+
if (page === 'providers') renderProviders();
|
|
95
|
+
if (page === 'stats') loadStats();
|
|
96
|
+
if (page === 'system-logs') loadLogs();
|
|
97
|
+
if (page === 'request-logs') renderRequestLogs();
|
|
98
|
+
if (page === 'assistant') populateAssistantProxySelect();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
document.querySelectorAll('.nav-item[data-page]').forEach(item => {
|
|
102
|
+
item.addEventListener('click', (e) => {
|
|
103
|
+
e.preventDefault();
|
|
104
|
+
navigateTo(item.dataset.page);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// ---------- Data Loading ----------
|
|
109
|
+
async function loadProxies() {
|
|
110
|
+
try {
|
|
111
|
+
const res = await fetch('/api/proxies');
|
|
112
|
+
proxies = await res.json();
|
|
113
|
+
document.getElementById('nav-proxy-count').textContent = proxies.length;
|
|
114
|
+
if (currentPage === 'proxies') renderProxies();
|
|
115
|
+
if (currentPage === 'dashboard') renderDashProxies();
|
|
116
|
+
updateDashStats();
|
|
117
|
+
populateProxyFilterOptions();
|
|
118
|
+
} catch (err) {
|
|
119
|
+
console.error('loadProxies error:', err);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function loadProviders() {
|
|
124
|
+
try {
|
|
125
|
+
const res = await fetch('/api/providers');
|
|
126
|
+
providers = await res.json();
|
|
127
|
+
document.getElementById('nav-provider-count').textContent = providers.length;
|
|
128
|
+
if (currentPage === 'providers') renderProviders();
|
|
129
|
+
if (currentPage === 'dashboard') renderDashProviderHealth();
|
|
130
|
+
updateDashStats();
|
|
131
|
+
populateProxyProviderSelect();
|
|
132
|
+
} catch (err) {
|
|
133
|
+
console.error('loadProviders error:', err);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async function loadKeyHealth() {
|
|
138
|
+
try {
|
|
139
|
+
const res = await fetch('/api/key-health');
|
|
140
|
+
keyHealth = await res.json();
|
|
141
|
+
renderDashProviderHealth();
|
|
142
|
+
updateTopbarHealth();
|
|
143
|
+
} catch (err) {
|
|
144
|
+
console.error('loadKeyHealth error:', err);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function loadStats() {
|
|
149
|
+
try {
|
|
150
|
+
const params = new URLSearchParams({ range: statsRange });
|
|
151
|
+
if (statsProxyId) params.set('proxyId', statsProxyId);
|
|
152
|
+
const start = document.getElementById('stats-start')?.value;
|
|
153
|
+
const end = document.getElementById('stats-end')?.value;
|
|
154
|
+
if (start) params.set('startDate', start);
|
|
155
|
+
if (end) params.set('endDate', end);
|
|
156
|
+
const res = await fetch('/api/stats?' + params);
|
|
157
|
+
const data = await res.json();
|
|
158
|
+
renderStats(data);
|
|
159
|
+
updateDashStats(data);
|
|
160
|
+
} catch (err) {
|
|
161
|
+
console.error('loadStats error:', err);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function loadLogs() {
|
|
166
|
+
try {
|
|
167
|
+
const lines = document.getElementById('log-lines')?.value || 200;
|
|
168
|
+
const res = await fetch('/api/logs?lines=' + lines);
|
|
169
|
+
const data = await res.json();
|
|
170
|
+
renderLogs(data.lines || []);
|
|
171
|
+
} catch (err) {
|
|
172
|
+
console.error('loadLogs error:', err);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function refreshDashboard() {
|
|
177
|
+
renderDashProxies();
|
|
178
|
+
renderDashProviderHealth();
|
|
179
|
+
renderDashRecentRequests();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ---------- Dashboard Rendering ----------
|
|
183
|
+
function updateDashStats(statsData) {
|
|
184
|
+
const running = proxies.filter(p => p.running).length;
|
|
185
|
+
document.getElementById('dash-running').textContent = running;
|
|
186
|
+
document.getElementById('dash-total').textContent = proxies.length;
|
|
187
|
+
|
|
188
|
+
if (statsData) {
|
|
189
|
+
document.getElementById('dash-tokens').textContent = formatTokens(statsData.summary?.total || 0);
|
|
190
|
+
document.getElementById('dash-tokens-sub').textContent = (statsData.summary?.hasEstimated ? '\u542b\u4f30\u7b97 ' : '') + '\u4eca\u65e5';
|
|
191
|
+
document.getElementById('dash-requests').textContent = (statsData.summary?.requests || 0).toLocaleString();
|
|
192
|
+
document.getElementById('dash-requests-sub').textContent = statsData.byProvider?.length + ' \u4e2a\u4f9b\u5e94\u5546';
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const healthStatuses = Object.values(keyHealth);
|
|
196
|
+
const unhealthy = healthStatuses.filter(h => h.status === 'unhealthy').length;
|
|
197
|
+
const partial = healthStatuses.filter(h => h.status === 'partial').length;
|
|
198
|
+
if (unhealthy > 0) {
|
|
199
|
+
document.getElementById('dash-health').textContent = unhealthy + '\u4e2a\u5f02\u5e38';
|
|
200
|
+
document.getElementById('dash-health').style.color = 'var(--error)';
|
|
201
|
+
document.getElementById('dash-health-sub').textContent = partial > 0 ? partial + ' \u4e2a\u90e8\u5206\u5f02\u5e38' : '\u9700\u8981\u5173\u6ce8';
|
|
202
|
+
} else if (partial > 0) {
|
|
203
|
+
document.getElementById('dash-health').textContent = partial + '\u4e2a\u8b66\u544a';
|
|
204
|
+
document.getElementById('dash-health').style.color = 'var(--warning)';
|
|
205
|
+
document.getElementById('dash-health-sub').textContent = '\u90e8\u5206 Key \u5f02\u5e38';
|
|
206
|
+
} else {
|
|
207
|
+
document.getElementById('dash-health').textContent = '\u6b63\u5e38';
|
|
208
|
+
document.getElementById('dash-health').style.color = 'var(--success)';
|
|
209
|
+
document.getElementById('dash-health-sub').textContent = '\u5168\u90e8\u4f9b\u5e94\u5546\u5065\u5eb7';
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function renderDashProxies() {
|
|
214
|
+
const container = document.getElementById('dash-proxy-list');
|
|
215
|
+
if (!container) return;
|
|
216
|
+
if (proxies.length === 0) {
|
|
217
|
+
container.innerHTML = '<div class="empty-sm">\u6682\u65e0\u4ee3\u7406\u914d\u7f6e</div>';
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
container.innerHTML = proxies.slice(0, 6).map(p => {
|
|
221
|
+
const provider = providers.find(pr => pr.id === p.providerId);
|
|
222
|
+
return `
|
|
223
|
+
<div class="proxy-mini-item" onclick="navigateTo('proxies')">
|
|
224
|
+
<div class="proxy-mini-status ${p.running ? 'running' : 'stopped'}"></div>
|
|
225
|
+
<div class="proxy-mini-info">
|
|
226
|
+
<div class="proxy-mini-name">${escapeHtml(p.name)}</div>
|
|
227
|
+
<div class="proxy-mini-meta">${escapeHtml(provider?.name || p.providerId)}</div>
|
|
228
|
+
</div>
|
|
229
|
+
<div class="proxy-mini-port">:${p.port}</div>
|
|
230
|
+
</div>
|
|
231
|
+
`;
|
|
232
|
+
}).join('');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function renderDashProviderHealth() {
|
|
236
|
+
const container = document.getElementById('dash-provider-health');
|
|
237
|
+
if (!container) return;
|
|
238
|
+
if (providers.length === 0) {
|
|
239
|
+
container.innerHTML = '<div class="empty-sm">\u6682\u65e0\u4f9b\u5e94\u5546</div>';
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
container.innerHTML = providers.map(p => {
|
|
243
|
+
const h = keyHealth[p.id];
|
|
244
|
+
let statusClass = 'unknown';
|
|
245
|
+
let statusText = '\u672a\u68c0\u6d4b';
|
|
246
|
+
if (h) {
|
|
247
|
+
if (h.status === 'healthy') { statusClass = 'healthy'; statusText = '\u6b63\u5e38'; }
|
|
248
|
+
else if (h.status === 'partial') { statusClass = 'partial'; statusText = '\u90e8\u5206\u5f02\u5e38'; }
|
|
249
|
+
else if (h.status === 'unhealthy') { statusClass = 'unhealthy'; statusText = '\u5f02\u5e38'; }
|
|
250
|
+
}
|
|
251
|
+
return `
|
|
252
|
+
<div class="provider-health-item">
|
|
253
|
+
<div class="provider-health-name">${escapeHtml(p.name)}</div>
|
|
254
|
+
<div class="provider-health-status ${statusClass}">${statusText}</div>
|
|
255
|
+
</div>
|
|
256
|
+
`;
|
|
257
|
+
}).join('');
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function renderDashRecentRequests() {
|
|
261
|
+
const tbody = document.getElementById('dash-recent-requests');
|
|
262
|
+
if (!tbody) return;
|
|
263
|
+
const recent = requestLogs.slice(0, 8);
|
|
264
|
+
if (recent.length === 0) {
|
|
265
|
+
tbody.innerHTML = '<tr><td colspan="6" class="empty-cell">\u6682\u65e0\u8bf7\u6c42\u8bb0\u5f55</td></tr>';
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
tbody.innerHTML = recent.map(r => renderRequestLogRow(r, true)).join('');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function updateTopbarHealth() {
|
|
272
|
+
const container = document.getElementById('topbar-health');
|
|
273
|
+
if (!container) return;
|
|
274
|
+
const statuses = Object.values(keyHealth);
|
|
275
|
+
const unhealthy = statuses.filter(s => s.status === 'unhealthy').length;
|
|
276
|
+
const partial = statuses.filter(s => s.status === 'partial').length;
|
|
277
|
+
if (unhealthy > 0) {
|
|
278
|
+
container.innerHTML = `<span class="health-dot error"></span>${unhealthy} \u4e2a\u4f9b\u5e94\u5546\u5f02\u5e38`;
|
|
279
|
+
} else if (partial > 0) {
|
|
280
|
+
container.innerHTML = `<span class="health-dot warn"></span>${partial} \u4e2a\u4f9b\u5e94\u5546\u8b66\u544a`;
|
|
281
|
+
} else if (statuses.length > 0) {
|
|
282
|
+
container.innerHTML = `<span class="health-dot ok"></span>\u5168\u90e8\u6b63\u5e38`;
|
|
283
|
+
} else {
|
|
284
|
+
container.innerHTML = '';
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ---------- Proxy Page ----------
|
|
289
|
+
function renderProxies() {
|
|
290
|
+
const grid = document.getElementById('proxy-grid');
|
|
291
|
+
if (!grid) return;
|
|
292
|
+
const search = (document.getElementById('proxy-search')?.value || '').toLowerCase();
|
|
293
|
+
const filtered = proxies.filter(p =>
|
|
294
|
+
!search ||
|
|
295
|
+
p.name.toLowerCase().includes(search) ||
|
|
296
|
+
String(p.port).includes(search) ||
|
|
297
|
+
(p.providerName || '').toLowerCase().includes(search)
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
if (filtered.length === 0 && proxies.length === 0) {
|
|
301
|
+
grid.innerHTML = `
|
|
302
|
+
<div class="empty-state">
|
|
303
|
+
<div class="empty-icon"><svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><polygon points="12 2 2 7 12 12 22 7 12 2"></polygon><polyline points="2 17 12 22 22 17"></polyline><polyline points="2 12 12 17 22 12"></polyline></svg></div>
|
|
304
|
+
<p>\u8fd8\u6ca1\u6709\u914d\u7f6e\u4ee3\u7406</p>
|
|
305
|
+
<button class="btn btn-primary" onclick="openProxyModal()">\u521b\u5efa\u7b2c\u4e00\u4e2a\u4ee3\u7406</button>
|
|
306
|
+
</div>`;
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (filtered.length === 0) {
|
|
310
|
+
grid.innerHTML = '<div class="empty-state"><p>\u6ca1\u6709\u5339\u914d\u7684\u4ee3\u7406</p></div>';
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
grid.innerHTML = filtered.map(p => {
|
|
315
|
+
const provider = providers.find(pr => pr.id === p.providerId);
|
|
316
|
+
return `
|
|
317
|
+
<div class="proxy-card ${p.running ? 'running' : 'stopped'}">
|
|
318
|
+
<div class="proxy-card-header">
|
|
319
|
+
<div class="proxy-card-title">${escapeHtml(p.name)}</div>
|
|
320
|
+
<span class="proxy-card-badge ${p.running ? 'running' : 'stopped'}">${p.running ? '\u8fd0\u884c\u4e2d' : '\u5df2\u505c\u6b62'}</span>
|
|
321
|
+
</div>
|
|
322
|
+
<div class="proxy-card-meta">
|
|
323
|
+
<div class="proxy-card-meta-item">
|
|
324
|
+
<div class="proxy-card-meta-label">\u7aef\u53e3</div>
|
|
325
|
+
<div class="proxy-card-meta-value">:${p.port}</div>
|
|
326
|
+
</div>
|
|
327
|
+
<div class="proxy-card-meta-item">
|
|
328
|
+
<div class="proxy-card-meta-label">\u4f9b\u5e94\u5546</div>
|
|
329
|
+
<div class="proxy-card-meta-value">${escapeHtml(provider?.name || p.providerId)}</div>
|
|
330
|
+
</div>
|
|
331
|
+
<div class="proxy-card-meta-item">
|
|
332
|
+
<div class="proxy-card-meta-label">\u534f\u8bae</div>
|
|
333
|
+
<div class="proxy-card-meta-value">${escapeHtml(p.protocol || provider?.protocol || 'openai')}</div>
|
|
334
|
+
</div>
|
|
335
|
+
<div class="proxy-card-meta-item">
|
|
336
|
+
<div class="proxy-card-meta-label">\u8def\u7531</div>
|
|
337
|
+
<div class="proxy-card-meta-value">${formatRouting(p.routingStrategy)}</div>
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
${p.defaultModel ? `<div style="margin-bottom:8px;font-size:12px;color:var(--text-muted);font-family:var(--font-mono)">\u9ed8\u8ba4\u6a21\u578b: ${escapeHtml(p.defaultModel)}</div>` : ''}
|
|
341
|
+
${p.providerPool && p.providerPool.length > 0 ? `<div class="proxy-pool-preview">\u5907\u9009: ${p.providerPool.map(item => { const fp = providers.find(x => x.id === item.providerId); return escapeHtml(fp?.name || item.providerId); }).join(' / ')}</div>` : ''}
|
|
342
|
+
<div class="proxy-card-actions">
|
|
343
|
+
${p.running
|
|
344
|
+
? `<button class="btn btn-sm" onclick="stopProxy('${p.id}')">\u505c\u6b62</button>`
|
|
345
|
+
: `<button class="btn btn-sm btn-primary" onclick="startProxy('${p.id}')">\u542f\u52a8</button>`
|
|
346
|
+
}
|
|
347
|
+
<button class="btn btn-sm" onclick="editProxy('${p.id}')">\u7f16\u8f91</button>
|
|
348
|
+
<button class="btn btn-sm" onclick="copyProxyUrl('${p.id}')">\u590d\u5236\u5730\u5740</button>
|
|
349
|
+
<button class="btn btn-sm" style="color:var(--error)" onclick="deleteProxy('${p.id}')">\u5220\u9664</button>
|
|
350
|
+
</div>
|
|
351
|
+
</div>
|
|
352
|
+
`;
|
|
353
|
+
}).join('');
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function filterProxies() {
|
|
357
|
+
renderProxies();
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function formatRouting(s) {
|
|
361
|
+
const map = {
|
|
362
|
+
primary_fallback: '\u4e3b\u5907',
|
|
363
|
+
round_robin: '\u8f6e\u8be2',
|
|
364
|
+
weighted: '\u52a0\u6743',
|
|
365
|
+
fastest: '\u6700\u5feb',
|
|
366
|
+
};
|
|
367
|
+
return map[s] || s;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
async function startProxy(id) {
|
|
371
|
+
try {
|
|
372
|
+
await fetch(`/api/proxies/${id}/start`, { method: 'POST' });
|
|
373
|
+
await loadProxies();
|
|
374
|
+
showToast('\u4ee3\u7406\u5df2\u542f\u52a8');
|
|
375
|
+
} catch (err) {
|
|
376
|
+
showToast('\u542f\u52a8\u5931\u8d25: ' + err.message, true);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
async function stopProxy(id) {
|
|
381
|
+
try {
|
|
382
|
+
await fetch(`/api/proxies/${id}/stop`, { method: 'POST' });
|
|
383
|
+
await loadProxies();
|
|
384
|
+
showToast('\u4ee3\u7406\u5df2\u505c\u6b62');
|
|
385
|
+
} catch (err) {
|
|
386
|
+
showToast('\u505c\u6b62\u5931\u8d25: ' + err.message, true);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async function startAllProxies() {
|
|
391
|
+
try {
|
|
392
|
+
const res = await fetch('/api/proxies/start-all', { method: 'POST' });
|
|
393
|
+
const data = await res.json();
|
|
394
|
+
await loadProxies();
|
|
395
|
+
const success = data.results?.filter(r => r.success).length || 0;
|
|
396
|
+
showToast(`\u542f\u52a8\u5b8c\u6210: ${success} / ${data.results?.length || 0}`);
|
|
397
|
+
} catch (err) {
|
|
398
|
+
showToast('\u6279\u91cf\u542f\u52a8\u5931\u8d25: ' + err.message, true);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
async function stopAllProxies() {
|
|
403
|
+
try {
|
|
404
|
+
await fetch('/api/proxies/stop-all', { method: 'POST' });
|
|
405
|
+
await loadProxies();
|
|
406
|
+
showToast('\u5168\u90e8\u4ee3\u7406\u5df2\u505c\u6b62');
|
|
407
|
+
} catch (err) {
|
|
408
|
+
showToast('\u6279\u91cf\u505c\u6b62\u5931\u8d25: ' + err.message, true);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
async function deleteProxy(id) {
|
|
413
|
+
const p = proxies.find(x => x.id === id);
|
|
414
|
+
if (!p) return;
|
|
415
|
+
const ok = await showConfirm(`\u786e\u5b9a\u5220\u9664\u4ee3\u7406 <strong>${escapeHtml(p.name)}</strong>\uff1f`);
|
|
416
|
+
if (!ok) return;
|
|
417
|
+
try {
|
|
418
|
+
await fetch(`/api/proxies/${id}`, { method: 'DELETE' });
|
|
419
|
+
await loadProxies();
|
|
420
|
+
showToast('\u4ee3\u7406\u5df2\u5220\u9664');
|
|
421
|
+
} catch (err) {
|
|
422
|
+
showToast('\u5220\u9664\u5931\u8d25: ' + err.message, true);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function copyProxyUrl(id) {
|
|
427
|
+
const p = proxies.find(x => x.id === id);
|
|
428
|
+
if (!p) return;
|
|
429
|
+
const url = `http://localhost:${p.port}`;
|
|
430
|
+
navigator.clipboard.writeText(url).then(() => showToast('\u5730\u5740\u5df2\u590d\u5236'));
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// ---------- Proxy Modal ----------
|
|
434
|
+
function openProxyModal() {
|
|
435
|
+
editingProxyId = null;
|
|
436
|
+
document.getElementById('proxy-modal-title').textContent = '\u65b0\u5efa\u4ee3\u7406';
|
|
437
|
+
document.getElementById('proxy-id').value = '';
|
|
438
|
+
document.getElementById('proxy-name').value = '';
|
|
439
|
+
document.getElementById('proxy-port').value = '';
|
|
440
|
+
document.getElementById('proxy-auth').value = 'false';
|
|
441
|
+
document.getElementById('proxy-auth-token').value = '';
|
|
442
|
+
document.getElementById('proxy-auth-token-group').style.display = 'none';
|
|
443
|
+
document.getElementById('proxy-provider').value = '';
|
|
444
|
+
document.getElementById('proxy-model').innerHTML = '<option value="">\u4f7f\u7528\u8bf7\u6c42\u6a21\u578b</option>';
|
|
445
|
+
document.getElementById('proxy-routing').value = 'primary_fallback';
|
|
446
|
+
document.getElementById('proxy-weight').value = '1';
|
|
447
|
+
providerPoolItems = [];
|
|
448
|
+
renderPoolEditor();
|
|
449
|
+
populateProxyProviderSelect();
|
|
450
|
+
showModal('proxy-modal');
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function editProxy(id) {
|
|
454
|
+
const p = proxies.find(x => x.id === id);
|
|
455
|
+
if (!p) return;
|
|
456
|
+
editingProxyId = id;
|
|
457
|
+
document.getElementById('proxy-modal-title').textContent = '\u7f16\u8f91\u4ee3\u7406';
|
|
458
|
+
document.getElementById('proxy-id').value = p.id;
|
|
459
|
+
document.getElementById('proxy-name').value = p.name;
|
|
460
|
+
document.getElementById('proxy-port').value = p.port;
|
|
461
|
+
document.getElementById('proxy-auth').value = p.requireAuth ? 'true' : 'false';
|
|
462
|
+
document.getElementById('proxy-auth-token').value = p.authToken || '';
|
|
463
|
+
document.getElementById('proxy-auth-token-group').style.display = p.requireAuth ? '' : 'none';
|
|
464
|
+
document.getElementById('proxy-provider').value = p.providerId || '';
|
|
465
|
+
document.getElementById('proxy-routing').value = p.routingStrategy || 'primary_fallback';
|
|
466
|
+
document.getElementById('proxy-weight').value = p.providerWeight || 1;
|
|
467
|
+
populateProxyProviderSelect();
|
|
468
|
+
updateProxyModelSelect(p.providerId, p.defaultModel);
|
|
469
|
+
providerPoolItems = Array.isArray(p.providerPool) ? p.providerPool.map(x => ({...x})) : [];
|
|
470
|
+
renderPoolEditor();
|
|
471
|
+
showModal('proxy-modal');
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function closeProxyModal() {
|
|
475
|
+
hideModal('proxy-modal');
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
function populateProxyProviderSelect() {
|
|
480
|
+
const select = document.getElementById('proxy-provider');
|
|
481
|
+
const current = select.value;
|
|
482
|
+
select.innerHTML = '<option value="">\u9009\u62e9\u4f9b\u5e94\u5546...</option>' +
|
|
483
|
+
providers.map(p => `<option value="${escapeHtml(p.id)}">${escapeHtml(p.name)}</option>`).join('');
|
|
484
|
+
select.value = current || '';
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function updateProxyModelSelect(providerId, selectedModel) {
|
|
488
|
+
const select = document.getElementById('proxy-model');
|
|
489
|
+
const provider = providers.find(p => p.id === providerId);
|
|
490
|
+
const models = provider?.models || [];
|
|
491
|
+
select.innerHTML = '<option value="">\u4f7f\u7528\u8bf7\u6c42\u6a21\u578b</option>' +
|
|
492
|
+
models.map(m => `<option value="${escapeHtml(m)}">${escapeHtml(m)}</option>`).join('');
|
|
493
|
+
if (selectedModel) select.value = selectedModel;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function renderPoolEditor() {
|
|
497
|
+
const container = document.getElementById('proxy-pool-editor');
|
|
498
|
+
if (providerPoolItems.length === 0) {
|
|
499
|
+
container.innerHTML = '<div class="pool-empty">\u6682\u65e0\u5907\u9009\u4f9b\u5e94\u5546</div>';
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
container.innerHTML = providerPoolItems.map((item, i) => {
|
|
503
|
+
const provider = providers.find(p => p.id === item.providerId);
|
|
504
|
+
const models = provider?.models || [];
|
|
505
|
+
return `
|
|
506
|
+
<div class="pool-item">
|
|
507
|
+
<select class="pool-item-select" onchange="updatePoolProvider(${i}, this.value)">
|
|
508
|
+
<option value="">\u9009\u62e9\u4f9b\u5e94\u5546...</option>
|
|
509
|
+
${providers.map(p => `<option value="${escapeHtml(p.id)}" ${p.id === item.providerId ? 'selected' : ''}>${escapeHtml(p.name)}</option>`).join('')}
|
|
510
|
+
</select>
|
|
511
|
+
<select class="pool-item-select" onchange="updatePoolModel(${i}, this.value)">
|
|
512
|
+
<option value="">\u9ed8\u8ba4\u6a21\u578b</option>
|
|
513
|
+
${models.map(m => `<option value="${escapeHtml(m)}" ${m === item.model ? 'selected' : ''}>${escapeHtml(m)}</option>`).join('')}
|
|
514
|
+
</select>
|
|
515
|
+
<input type="number" min="1" value="${item.weight || 1}" onchange="updatePoolWeight(${i}, this.value)" style="width:50px" title="\u6743\u91cd">
|
|
516
|
+
<button type="button" class="pool-item-remove" onclick="removePoolItem(${i})">×</button>
|
|
517
|
+
</div>
|
|
518
|
+
`;
|
|
519
|
+
}).join('');
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
function addPoolItem() {
|
|
523
|
+
if (providers.length === 0) {
|
|
524
|
+
showToast('\u8bf7\u5148\u521b\u5efa\u4f9b\u5e94\u5546', true);
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
providerPoolItems.push({ providerId: '', model: '', weight: 1 });
|
|
528
|
+
renderPoolEditor();
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function updatePoolProvider(index, providerId) {
|
|
532
|
+
providerPoolItems[index].providerId = providerId;
|
|
533
|
+
providerPoolItems[index].model = '';
|
|
534
|
+
renderPoolEditor();
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function updatePoolModel(index, model) {
|
|
538
|
+
providerPoolItems[index].model = model;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function removePoolItem(index) {
|
|
542
|
+
providerPoolItems.splice(index, 1);
|
|
543
|
+
renderPoolEditor();
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
function updatePoolWeight(index, value) {
|
|
547
|
+
providerPoolItems[index].weight = Math.max(1, parseInt(value) || 1);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
async function handleProxySubmit(e) {
|
|
551
|
+
e.preventDefault();
|
|
552
|
+
const payload = {
|
|
553
|
+
name: document.getElementById('proxy-name').value.trim(),
|
|
554
|
+
port: parseInt(document.getElementById('proxy-port').value),
|
|
555
|
+
requireAuth: document.getElementById('proxy-auth').value === 'true',
|
|
556
|
+
authToken: document.getElementById('proxy-auth-token').value.trim() || null,
|
|
557
|
+
providerId: document.getElementById('proxy-provider').value,
|
|
558
|
+
defaultModel: document.getElementById('proxy-model').value,
|
|
559
|
+
routingStrategy: document.getElementById('proxy-routing').value,
|
|
560
|
+
providerWeight: parseInt(document.getElementById('proxy-weight').value) || 1,
|
|
561
|
+
providerPool: providerPoolItems,
|
|
562
|
+
};
|
|
563
|
+
if (!payload.name || !payload.port || !payload.providerId) {
|
|
564
|
+
showToast('\u8bf7\u586b\u5199\u5b8c\u6574\u4fe1\u606f', true);
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
try {
|
|
568
|
+
if (editingProxyId) {
|
|
569
|
+
await fetch(`/api/proxies/${editingProxyId}`, {
|
|
570
|
+
method: 'PUT',
|
|
571
|
+
headers: { 'Content-Type': 'application/json' },
|
|
572
|
+
body: JSON.stringify(payload),
|
|
573
|
+
});
|
|
574
|
+
showToast('\u4ee3\u7406\u5df2\u66f4\u65b0');
|
|
575
|
+
} else {
|
|
576
|
+
await fetch('/api/proxies', {
|
|
577
|
+
method: 'POST',
|
|
578
|
+
headers: { 'Content-Type': 'application/json' },
|
|
579
|
+
body: JSON.stringify(payload),
|
|
580
|
+
});
|
|
581
|
+
showToast('\u4ee3\u7406\u5df2\u521b\u5efa');
|
|
582
|
+
}
|
|
583
|
+
closeProxyModal();
|
|
584
|
+
await loadProxies();
|
|
585
|
+
} catch (err) {
|
|
586
|
+
showToast('\u4fdd\u5b58\u5931\u8d25: ' + err.message, true);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
async function testConnectionFromModal() {
|
|
591
|
+
const providerId = document.getElementById('proxy-provider').value;
|
|
592
|
+
if (!providerId) {
|
|
593
|
+
showToast('\u8bf7\u5148\u9009\u62e9\u4f9b\u5e94\u5546', true);
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
await testProviderConnection(providerId);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// ---------- Provider Page ----------
|
|
600
|
+
function renderProviders() {
|
|
601
|
+
const grid = document.getElementById('provider-grid');
|
|
602
|
+
if (!grid) return;
|
|
603
|
+
const search = (document.getElementById('provider-search')?.value || '').toLowerCase();
|
|
604
|
+
const filtered = providers.filter(p =>
|
|
605
|
+
!search ||
|
|
606
|
+
p.name.toLowerCase().includes(search) ||
|
|
607
|
+
p.url.toLowerCase().includes(search)
|
|
608
|
+
);
|
|
609
|
+
|
|
610
|
+
if (filtered.length === 0 && providers.length === 0) {
|
|
611
|
+
grid.innerHTML = `
|
|
612
|
+
<div class="empty-state">
|
|
613
|
+
<div class="empty-icon"><svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"></path><polyline points="3.27 6.96 12 12.01 20.73 6.96"></polyline><line x1="12" y1="22.08" x2="12" y2="12"></line></svg></div>
|
|
614
|
+
<p>\u8fd8\u6ca1\u6709\u914d\u7f6e\u4f9b\u5e94\u5546</p>
|
|
615
|
+
<button class="btn btn-primary" onclick="openProviderModal()">\u521b\u5efa\u7b2c\u4e00\u4e2a\u4f9b\u5e94\u5546</button>
|
|
616
|
+
</div>`;
|
|
617
|
+
return;
|
|
618
|
+
}
|
|
619
|
+
if (filtered.length === 0) {
|
|
620
|
+
grid.innerHTML = '<div class="empty-state"><p>\u6ca1\u6709\u5339\u914d\u7684\u4f9b\u5e94\u5546</p></div>';
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
grid.innerHTML = filtered.map(p => {
|
|
625
|
+
const h = keyHealth[p.id];
|
|
626
|
+
let statusDot = '';
|
|
627
|
+
if (h) {
|
|
628
|
+
const color = h.status === 'healthy' ? 'var(--success)' : h.status === 'partial' ? 'var(--warning)' : 'var(--error)';
|
|
629
|
+
statusDot = `<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:${color};margin-left:8px;"></span>`;
|
|
630
|
+
}
|
|
631
|
+
return `
|
|
632
|
+
<div class="provider-card">
|
|
633
|
+
<div class="provider-card-header">
|
|
634
|
+
<div class="provider-card-name">${escapeHtml(p.name)}${statusDot}</div>
|
|
635
|
+
<span class="provider-card-protocol">${escapeHtml(p.protocol)}</span>
|
|
636
|
+
</div>
|
|
637
|
+
<div class="provider-card-url">${escapeHtml(p.url)}</div>
|
|
638
|
+
<div class="provider-card-models">
|
|
639
|
+
${(p.models || []).slice(0, 6).map(m => `<span class="provider-card-model">${escapeHtml(m)}</span>`).join('')}
|
|
640
|
+
${(p.models || []).length > 6 ? `<span class="provider-card-model">+${p.models.length - 6}</span>` : ''}
|
|
641
|
+
</div>
|
|
642
|
+
<div class="provider-card-keys">${(p.apiKeys || []).length} \u4e2a API Key</div>
|
|
643
|
+
<div class="provider-card-actions">
|
|
644
|
+
<button class="btn btn-sm" onclick="editProvider('${p.id}')">\u7f16\u8f91</button>
|
|
645
|
+
<button class="btn btn-sm" onclick="testProviderConnection('${p.id}')">\u6d4b\u8bd5</button>
|
|
646
|
+
<button class="btn btn-sm" style="color:var(--error)" onclick="deleteProvider('${p.id}')">\u5220\u9664</button>
|
|
647
|
+
</div>
|
|
648
|
+
</div>
|
|
649
|
+
`;
|
|
650
|
+
}).join('');
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function filterProviders() {
|
|
654
|
+
renderProviders();
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function openProviderModal() {
|
|
658
|
+
editingProviderId = null;
|
|
659
|
+
document.getElementById('provider-modal-title').textContent = '\u65b0\u5efa\u4f9b\u5e94\u5546';
|
|
660
|
+
document.getElementById('provider-edit-id').value = '';
|
|
661
|
+
document.getElementById('provider-name').value = '';
|
|
662
|
+
document.getElementById('provider-protocol').value = 'openai';
|
|
663
|
+
document.getElementById('provider-url').value = '';
|
|
664
|
+
providerModelTags = [];
|
|
665
|
+
renderModelTags();
|
|
666
|
+
providerKeys = [{ key: '', alias: '', index: 0, enabled: true }];
|
|
667
|
+
renderProviderKeys();
|
|
668
|
+
document.getElementById('provider-azure-row').style.display = 'none';
|
|
669
|
+
document.getElementById('provider-azure-deployment').value = '';
|
|
670
|
+
document.getElementById('provider-azure-version').value = '';
|
|
671
|
+
showModal('provider-modal');
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function editProvider(id) {
|
|
675
|
+
const p = providers.find(x => x.id === id);
|
|
676
|
+
if (!p) return;
|
|
677
|
+
editingProviderId = id;
|
|
678
|
+
document.getElementById('provider-modal-title').textContent = '\u7f16\u8f91\u4f9b\u5e94\u5546';
|
|
679
|
+
document.getElementById('provider-edit-id').value = p.id;
|
|
680
|
+
document.getElementById('provider-name').value = p.name;
|
|
681
|
+
document.getElementById('provider-protocol').value = p.protocol || 'openai';
|
|
682
|
+
document.getElementById('provider-url').value = p.url;
|
|
683
|
+
providerModelTags = [...(p.models || [])];
|
|
684
|
+
renderModelTags();
|
|
685
|
+
providerKeys = (p.apiKeys || []).map((k, i) => ({ ...k, index: typeof k.index === 'number' ? k.index : i }));
|
|
686
|
+
if (providerKeys.length === 0) providerKeys = [{ key: '', alias: '', index: 0 }];
|
|
687
|
+
renderProviderKeys();
|
|
688
|
+
const isAzure = p.protocol === 'openai' && p.azureDeployment;
|
|
689
|
+
document.getElementById('provider-azure-row').style.display = isAzure ? 'grid' : 'none';
|
|
690
|
+
document.getElementById('provider-azure-deployment').value = p.azureDeployment || '';
|
|
691
|
+
document.getElementById('provider-azure-version').value = p.azureApiVersion || '';
|
|
692
|
+
showModal('provider-modal');
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
function closeProviderModal() {
|
|
696
|
+
hideModal('provider-modal');
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
function renderModelTags() {
|
|
700
|
+
const list = document.getElementById('provider-models-list');
|
|
701
|
+
list.innerHTML = providerModelTags.map((tag, i) => `
|
|
702
|
+
<span class="tag">${escapeHtml(tag)}<button type="button" class="tag-remove" onclick="removeModelTag(${i})">×</button></span>
|
|
703
|
+
`).join('');
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function handleModelTagInput(e) {
|
|
707
|
+
if (e.key === 'Enter') {
|
|
708
|
+
e.preventDefault();
|
|
709
|
+
const val = e.target.value.trim();
|
|
710
|
+
if (val && !providerModelTags.includes(val)) {
|
|
711
|
+
providerModelTags.push(val);
|
|
712
|
+
renderModelTags();
|
|
713
|
+
e.target.value = '';
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function removeModelTag(index) {
|
|
719
|
+
providerModelTags.splice(index, 1);
|
|
720
|
+
renderModelTags();
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function renderProviderKeys() {
|
|
724
|
+
const container = document.getElementById('provider-keys-list');
|
|
725
|
+
container.innerHTML = providerKeys.map((k, i) => `
|
|
726
|
+
<div class="key-row" data-index="${k.index ?? i}" data-masked="${k.masked ? 'true' : 'false'}">
|
|
727
|
+
<input type="text" placeholder="\u522b\u540d" value="${escapeHtml(k.alias || '')}" oninput="providerKeys[${i}].alias = this.value">
|
|
728
|
+
${k.masked
|
|
729
|
+
? `<span class="api-key-masked" data-idx="${i}">sk-••••••••</span>`
|
|
730
|
+
: `<input type="password" placeholder="sk-..." value="${escapeHtml(k.key || '')}" oninput="providerKeys[${i}].key = this.value">`
|
|
731
|
+
}
|
|
732
|
+
<label class="toggle-switch" title="${k.enabled !== false ? '\u5df2\u542f\u7528' : '\u5df2\u7981\u7528'}">
|
|
733
|
+
<input type="checkbox" ${k.enabled !== false ? 'checked' : ''} onchange="providerKeys[${i}].enabled = this.checked">
|
|
734
|
+
<span class="toggle-slider"></span>
|
|
735
|
+
</label>
|
|
736
|
+
<button type="button" class="btn btn-sm" onclick="removeProviderKey(${i})">\u79fb\u9664</button>
|
|
737
|
+
</div>
|
|
738
|
+
`).join('');
|
|
739
|
+
attachMaskedKeyClicks();
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function addProviderKey() {
|
|
743
|
+
providerKeys.push({ key: '', alias: '', index: providerKeys.length, enabled: true });
|
|
744
|
+
renderProviderKeys();
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function removeProviderKey(index) {
|
|
748
|
+
providerKeys.splice(index, 1);
|
|
749
|
+
if (providerKeys.length === 0) providerKeys.push({ key: '', alias: '', index: 0 });
|
|
750
|
+
renderProviderKeys();
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
function attachMaskedKeyClicks() {
|
|
754
|
+
document.querySelectorAll('.api-key-masked').forEach(span => {
|
|
755
|
+
if (span._attached) return;
|
|
756
|
+
span._attached = true;
|
|
757
|
+
span.title = '点击修改';
|
|
758
|
+
span.addEventListener('click', () => {
|
|
759
|
+
const i = parseInt(span.dataset.idx, 10);
|
|
760
|
+
const row = span.closest('.key-row');
|
|
761
|
+
const group = span.parentElement;
|
|
762
|
+
|
|
763
|
+
// Replace span with input
|
|
764
|
+
const input = document.createElement('input');
|
|
765
|
+
input.type = 'password';
|
|
766
|
+
input.className = 'key-input';
|
|
767
|
+
input.placeholder = '输入新的 API Key...';
|
|
768
|
+
group.replaceChild(input, span);
|
|
769
|
+
input.focus();
|
|
770
|
+
|
|
771
|
+
input.addEventListener('blur', () => {
|
|
772
|
+
const val = input.value.trim();
|
|
773
|
+
if (!val) {
|
|
774
|
+
// Restore masked span
|
|
775
|
+
const restored = document.createElement('span');
|
|
776
|
+
restored.className = 'api-key-masked';
|
|
777
|
+
restored.dataset.idx = i;
|
|
778
|
+
restored.textContent = 'sk-••••••••';
|
|
779
|
+
group.replaceChild(restored, input);
|
|
780
|
+
attachMaskedKeyClicks();
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
// Mark as edited — replace with password input
|
|
784
|
+
const newInput = document.createElement('input');
|
|
785
|
+
newInput.type = 'password';
|
|
786
|
+
newInput.className = 'key-input';
|
|
787
|
+
newInput.value = val;
|
|
788
|
+
group.replaceChild(newInput, input);
|
|
789
|
+
newInput.addEventListener('input', () => { providerKeys[i].key = newInput.value; });
|
|
790
|
+
providerKeys[i].key = val;
|
|
791
|
+
providerKeys[i].masked = false;
|
|
792
|
+
if (row) row.dataset.masked = 'false';
|
|
793
|
+
});
|
|
794
|
+
});
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function collectProviderKeys() {
|
|
799
|
+
return providerKeys.map(k => {
|
|
800
|
+
const alias = (k.alias || '').trim();
|
|
801
|
+
const enabled = k.enabled !== false;
|
|
802
|
+
if (k.masked) {
|
|
803
|
+
// Existing key: if key was edited, send key; otherwise preserve
|
|
804
|
+
if (k.key && !k.masked) return { key: k.key.trim(), alias, enabled };
|
|
805
|
+
return { alias, masked: true, index: k.index, enabled };
|
|
806
|
+
}
|
|
807
|
+
const key = (k.key || '').trim();
|
|
808
|
+
if (!key) return null;
|
|
809
|
+
return { key, alias, enabled };
|
|
810
|
+
}).filter(Boolean);
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
async function handleProviderSubmit(e) {
|
|
814
|
+
e.preventDefault();
|
|
815
|
+
const payload = {
|
|
816
|
+
name: document.getElementById('provider-name').value.trim(),
|
|
817
|
+
protocol: document.getElementById('provider-protocol').value,
|
|
818
|
+
url: document.getElementById('provider-url').value.trim(),
|
|
819
|
+
models: providerModelTags,
|
|
820
|
+
apiKeys: collectProviderKeys(),
|
|
821
|
+
};
|
|
822
|
+
if (payload.protocol === 'openai') {
|
|
823
|
+
const azDep = document.getElementById('provider-azure-deployment').value.trim();
|
|
824
|
+
if (azDep) {
|
|
825
|
+
payload.azureDeployment = azDep;
|
|
826
|
+
payload.azureApiVersion = document.getElementById('provider-azure-version').value.trim() || '2024-02-01';
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
if (!payload.name || !payload.url) {
|
|
830
|
+
showToast('\u8bf7\u586b\u5199\u5b8c\u6574\u4fe1\u606f', true);
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
try {
|
|
834
|
+
if (editingProviderId) {
|
|
835
|
+
await fetch(`/api/providers/${editingProviderId}`, {
|
|
836
|
+
method: 'PUT',
|
|
837
|
+
headers: { 'Content-Type': 'application/json' },
|
|
838
|
+
body: JSON.stringify(payload),
|
|
839
|
+
});
|
|
840
|
+
showToast('\u4f9b\u5e94\u5546\u5df2\u66f4\u65b0');
|
|
841
|
+
} else {
|
|
842
|
+
await fetch('/api/providers', {
|
|
843
|
+
method: 'POST',
|
|
844
|
+
headers: { 'Content-Type': 'application/json' },
|
|
845
|
+
body: JSON.stringify(payload),
|
|
846
|
+
});
|
|
847
|
+
showToast('\u4f9b\u5e94\u5546\u5df2\u521b\u5efa');
|
|
848
|
+
}
|
|
849
|
+
closeProviderModal();
|
|
850
|
+
await loadProviders();
|
|
851
|
+
} catch (err) {
|
|
852
|
+
showToast('\u4fdd\u5b58\u5931\u8d25: ' + err.message, true);
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
async function deleteProvider(id) {
|
|
857
|
+
const p = providers.find(x => x.id === id);
|
|
858
|
+
if (!p) return;
|
|
859
|
+
const ok = await showConfirm(`\u786e\u5b9a\u5220\u9664\u4f9b\u5e94\u5546 <strong>${escapeHtml(p.name)}</strong>\uff1f`);
|
|
860
|
+
if (!ok) return;
|
|
861
|
+
try {
|
|
862
|
+
const res = await fetch(`/api/providers/${id}`, { method: 'DELETE' });
|
|
863
|
+
if (!res.ok) {
|
|
864
|
+
const data = await res.json();
|
|
865
|
+
showToast(data.error || '\u5220\u9664\u5931\u8d25', true);
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
await loadProviders();
|
|
869
|
+
showToast('\u4f9b\u5e94\u5546\u5df2\u5220\u9664');
|
|
870
|
+
} catch (err) {
|
|
871
|
+
showToast('\u5220\u9664\u5931\u8d25: ' + err.message, true);
|
|
872
|
+
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
async function fetchModelsForProvider(el) {
|
|
876
|
+
const url = document.getElementById('provider-url').value.trim();
|
|
877
|
+
const protocol = document.getElementById('provider-protocol').value;
|
|
878
|
+
if (!url) {
|
|
879
|
+
showToast('\u8bf7\u5148\u586b\u5199 API \u5730\u5740', true);
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
const btn = el || document.activeElement;
|
|
883
|
+
if (btn) { btn.disabled = true; btn.textContent = '\u83b7\u53d6\u4e2d...'; }
|
|
884
|
+
try {
|
|
885
|
+
const key = providerKeys.find(k => k.key.trim())?.key.trim() || '';
|
|
886
|
+
const payload = { url, protocol, apiKey: key };
|
|
887
|
+
const azureDep = document.getElementById('provider-azure-deployment')?.value?.trim();
|
|
888
|
+
if (azureDep) {
|
|
889
|
+
payload.azureDeployment = azureDep;
|
|
890
|
+
payload.azureApiVersion = document.getElementById('provider-azure-version')?.value?.trim() || '2024-02-01';
|
|
891
|
+
}
|
|
892
|
+
const res = await fetch('/api/providers/available-models', {
|
|
893
|
+
method: 'POST',
|
|
894
|
+
headers: { 'Content-Type': 'application/json' },
|
|
895
|
+
body: JSON.stringify(payload),
|
|
896
|
+
});
|
|
897
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
898
|
+
const data = await res.json();
|
|
899
|
+
const models = data.models || [];
|
|
900
|
+
const existing = new Set(providerModelTags);
|
|
901
|
+
const newModels = models.filter(m => !existing.has(m));
|
|
902
|
+
providerModelTags.push(...newModels);
|
|
903
|
+
renderModelTags();
|
|
904
|
+
showToast(`\u5df2\u5bfc\u5165 ${newModels.length} \u4e2a\u6a21\u578b`);
|
|
905
|
+
} catch (err) {
|
|
906
|
+
showToast('\u83b7\u53d6\u5931\u8d25: ' + err.message, true);
|
|
907
|
+
} finally {
|
|
908
|
+
if (btn) { btn.disabled = false; btn.textContent = '\u81ea\u52a8\u83b7\u53d6\u6a21\u578b\u5217\u8868'; }
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
async function testProviderConnection(id, opts) {
|
|
913
|
+
const p = providers.find(x => x.id === id);
|
|
914
|
+
if (!p) return;
|
|
915
|
+
try {
|
|
916
|
+
const res = await fetch(`/api/providers/${id}/test`, {
|
|
917
|
+
method: 'POST',
|
|
918
|
+
headers: { 'Content-Type': 'application/json' },
|
|
919
|
+
body: JSON.stringify({
|
|
920
|
+
apiKeys: opts?.apiKeys || p.apiKeys,
|
|
921
|
+
models: p.models,
|
|
922
|
+
protocol: opts?.protocol || p.protocol,
|
|
923
|
+
}),
|
|
924
|
+
});
|
|
925
|
+
const data = await res.json();
|
|
926
|
+
showTestResult(data.ok, `${data.passed || 0}/${data.results?.length || 0} 个 Key 正常`, data.results || []);
|
|
927
|
+
} catch (err) {
|
|
928
|
+
showTestResult(false, '测试失败: ' + err.message, []);
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
async function testProviderFromModal() {
|
|
933
|
+
const url = document.getElementById('provider-url').value.trim();
|
|
934
|
+
const protocol = document.getElementById('provider-protocol').value;
|
|
935
|
+
if (!url) {
|
|
936
|
+
showToast('\u8bf7\u5148\u586b\u5199 API \u5730\u5740', true);
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
const isNew = !editingProviderId;
|
|
940
|
+
if (isNew) {
|
|
941
|
+
// \u65b0\u5efa\u4f9b\u5e94\u5546\uff1a\u53ea\u6d4b\u65b0\u8f93\u5165\u7684 key\uff0c\u7528 /api/test-connection
|
|
942
|
+
const apiKeys = collectProviderKeys()
|
|
943
|
+
.filter(k => !k.masked && k.key)
|
|
944
|
+
.map(k => ({ key: k.key.trim(), alias: k.alias || '' }));
|
|
945
|
+
await testProviderConnectionDirect({
|
|
946
|
+
url,
|
|
947
|
+
protocol,
|
|
948
|
+
apiKeys,
|
|
949
|
+
models: providerModelTags,
|
|
950
|
+
azureDeployment: document.getElementById('provider-azure-deployment').value.trim(),
|
|
951
|
+
azureApiVersion: document.getElementById('provider-azure-version').value.trim(),
|
|
952
|
+
});
|
|
953
|
+
} else {
|
|
954
|
+
// \u7f16\u8f91\u5df2\u6709\u4f9b\u5e94\u5546\uff1a\u6536\u96c6\u8868\u5355\u4e2d\u7684\u6240\u6709 key\uff08\u542b\u65b0\u589e\u672a\u4fdd\u5b58\u7684\uff09\uff0c\u53d1\u7ed9\u540e\u7aef\u6d4b\u8bd5
|
|
955
|
+
const apiKeys = collectProviderKeys();
|
|
956
|
+
const protocol = document.getElementById('provider-protocol').value;
|
|
957
|
+
await testProviderConnection(editingProviderId, { apiKeys, protocol });
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
async function testProviderConnectionDirect(provider) {
|
|
962
|
+
const keys = (provider.apiKeys || []).filter(k => k.key);
|
|
963
|
+
if (keys.length === 0) {
|
|
964
|
+
showTestResult(false, '\u6ca1\u6709\u53ef\u7528\u7684 API Key', []);
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
try {
|
|
968
|
+
const res = await fetch('/api/test-connection', {
|
|
969
|
+
method: 'POST',
|
|
970
|
+
headers: { 'Content-Type': 'application/json' },
|
|
971
|
+
body: JSON.stringify({
|
|
972
|
+
url: provider.url,
|
|
973
|
+
protocol: provider.protocol,
|
|
974
|
+
apiKeys: keys,
|
|
975
|
+
models: provider.models,
|
|
976
|
+
azureDeployment: provider.azureDeployment,
|
|
977
|
+
azureApiVersion: provider.azureApiVersion,
|
|
978
|
+
}),
|
|
979
|
+
});
|
|
980
|
+
const data = await res.json();
|
|
981
|
+
const passed = data.passed || 0;
|
|
982
|
+
const total = data.results?.length || 0;
|
|
983
|
+
showTestResult(data.ok, `${passed}/${total} \u4e2a Key \u6b63\u5e38`, data.results || []);
|
|
984
|
+
} catch (err) {
|
|
985
|
+
showTestResult(false, '\u6d4b\u8bd5\u5931\u8d25: ' + err.message, []);
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
function showTestResult(ok, summary, results) {
|
|
990
|
+
document.getElementById('test-summary').innerHTML = ok
|
|
991
|
+
? `<span style="color:var(--success)">\u2713 ${escapeHtml(summary)}</span>`
|
|
992
|
+
: `<span style="color:var(--error)">\u2717 ${escapeHtml(summary)}</span>`;
|
|
993
|
+
document.getElementById('test-details').innerHTML = results.map(r => `
|
|
994
|
+
<div style="display:flex;align-items:center;gap:8px;padding:8px 0;border-bottom:1px solid var(--border-subtle);font-size:13px">
|
|
995
|
+
<span style="color:${r.ok ? 'var(--success)' : 'var(--error)'};font-weight:600">${r.ok ? '\u2713' : '\u2717'}</span>
|
|
996
|
+
<span style="flex:1">${escapeHtml(r.alias || '\u672a\u547d\u540d')}</span>
|
|
997
|
+
${r.latency ? `<span style="color:var(--text-muted);font-family:var(--font-mono);font-size:12px">${r.latency}ms</span>` : ''}
|
|
998
|
+
${r.message ? `<span style="color:var(--error);font-size:12px">${escapeHtml(r.message)}</span>` : ''}
|
|
999
|
+
</div>
|
|
1000
|
+
`).join('');
|
|
1001
|
+
showModal('test-modal');
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
function closeTestModal() {
|
|
1005
|
+
hideModal('test-modal');
|
|
1006
|
+
}
|
|
1007
|
+
// ---------- Stats Page ----------
|
|
1008
|
+
function changeStatsRange(range) {
|
|
1009
|
+
statsRange = range;
|
|
1010
|
+
loadStats();
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
function changeStatsProxy(proxyId) {
|
|
1014
|
+
statsProxyId = proxyId;
|
|
1015
|
+
loadStats();
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
function renderStats(data) {
|
|
1019
|
+
const summary = data.summary || {};
|
|
1020
|
+
document.getElementById('stats-total').textContent = formatTokens(summary.total || 0);
|
|
1021
|
+
document.getElementById('stats-prompt').textContent = formatTokens(summary.prompt || 0);
|
|
1022
|
+
document.getElementById('stats-completion').textContent = formatTokens(summary.completion || 0);
|
|
1023
|
+
document.getElementById('stats-requests').textContent = (summary.requests || 0).toLocaleString();
|
|
1024
|
+
document.getElementById('stats-estimated-badge').style.display = summary.hasEstimated ? 'inline' : 'none';
|
|
1025
|
+
|
|
1026
|
+
const tbody = document.getElementById('stats-table-body');
|
|
1027
|
+
const byModel = data.byModel || [];
|
|
1028
|
+
if (byModel.length === 0) {
|
|
1029
|
+
tbody.innerHTML = '<tr><td colspan="6" class="empty-cell">\u6682\u65e0\u6570\u636e</td></tr>';
|
|
1030
|
+
return;
|
|
1031
|
+
}
|
|
1032
|
+
tbody.innerHTML = byModel.map(item => {
|
|
1033
|
+
const prefix = item.hasEstimated ? '~' : '';
|
|
1034
|
+
return `
|
|
1035
|
+
<tr>
|
|
1036
|
+
<td>${escapeHtml(item.provider)}</td>
|
|
1037
|
+
<td><code>${escapeHtml(item.model)}</code></td>
|
|
1038
|
+
<td class="num">${item.requests.toLocaleString()}</td>
|
|
1039
|
+
<td class="num">${prefix}${formatTokens(item.prompt)}</td>
|
|
1040
|
+
<td class="num">${prefix}${formatTokens(item.completion)}</td>
|
|
1041
|
+
<td class="num">${prefix}${formatTokens(item.total)}</td>
|
|
1042
|
+
</tr>
|
|
1043
|
+
`;
|
|
1044
|
+
}).join('');
|
|
1045
|
+
}
|
|
1046
|
+
|
|
1047
|
+
function populateProxyFilterOptions() {
|
|
1048
|
+
const selects = ['stats-proxy-filter', 'rq-proxy-filter'];
|
|
1049
|
+
selects.forEach(id => {
|
|
1050
|
+
const select = document.getElementById(id);
|
|
1051
|
+
if (!select) return;
|
|
1052
|
+
const current = select.value;
|
|
1053
|
+
select.innerHTML = '<option value="">\u5168\u90e8\u4ee3\u7406</option>' +
|
|
1054
|
+
proxies.map(p => `<option value="${escapeHtml(p.id)}">${escapeHtml(p.name)}</option>`).join('');
|
|
1055
|
+
select.value = current || '';
|
|
1056
|
+
});
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
async function exportStatsCSV() {
|
|
1060
|
+
try {
|
|
1061
|
+
const params = new URLSearchParams({ range: statsRange });
|
|
1062
|
+
if (statsProxyId) params.set('proxyId', statsProxyId);
|
|
1063
|
+
const start = document.getElementById('stats-start')?.value;
|
|
1064
|
+
const end = document.getElementById('stats-end')?.value;
|
|
1065
|
+
if (start) params.set('startDate', start);
|
|
1066
|
+
if (end) params.set('endDate', end);
|
|
1067
|
+
const res = await fetch('/api/stats?' + params);
|
|
1068
|
+
const data = await res.json();
|
|
1069
|
+
if (!data.byModel || data.byModel.length === 0) {
|
|
1070
|
+
showToast('\u5f53\u524d\u7b5b\u9009\u6761\u4ef6\u4e0b\u65e0\u6570\u636e\u53ef\u5bfc\u51fa', true);
|
|
1071
|
+
return;
|
|
1072
|
+
}
|
|
1073
|
+
const rows = [['\u4f9b\u5e94\u5546', '\u6a21\u578b', '\u8bf7\u6c42\u6570', '\u8f93\u5165Token', '\u8f93\u51faToken', '\u5408\u8ba1Token', '\u542b\u4f30\u7b97']];
|
|
1074
|
+
for (const item of data.byModel) {
|
|
1075
|
+
rows.push([item.provider, item.model, item.requests, item.prompt, item.completion, item.total, item.hasEstimated ? '\u662f' : '\u5426']);
|
|
1076
|
+
}
|
|
1077
|
+
const s = data.summary;
|
|
1078
|
+
rows.push(['\u5408\u8ba1', '', s.requests, s.prompt, s.completion, s.total, s.hasEstimated ? '\u662f' : '\u5426']);
|
|
1079
|
+
const csv = '\ufeff' + rows.map(r => r.map(c => `"${String(c).replace(/"/g, '""')}"`).join(',')).join('\n');
|
|
1080
|
+
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' });
|
|
1081
|
+
const url = URL.createObjectURL(blob);
|
|
1082
|
+
const a = document.createElement('a');
|
|
1083
|
+
a.href = url;
|
|
1084
|
+
a.download = `stats-${statsRange}-${new Date().toISOString().slice(0,10)}.csv`;
|
|
1085
|
+
a.click();
|
|
1086
|
+
URL.revokeObjectURL(url);
|
|
1087
|
+
} catch (err) {
|
|
1088
|
+
showToast('\u5bfc\u51fa\u5931\u8d25: ' + err.message, true);
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
// ---------- Request Logs Page ----------
|
|
1093
|
+
function connectRequestLogWS() {
|
|
1094
|
+
const wsProto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
1095
|
+
const wsUrl = `${wsProto}//${location.host}`;
|
|
1096
|
+
// Fallback: if no WS endpoint, use polling via a custom endpoint or skip
|
|
1097
|
+
// Since the backend may not have WS, we'll use a polling approach with the existing API
|
|
1098
|
+
// Actually, the original code used WebSocket. Let's try to connect.
|
|
1099
|
+
try {
|
|
1100
|
+
ws = new WebSocket(wsUrl);
|
|
1101
|
+
ws.onopen = () => {
|
|
1102
|
+
const btn = document.getElementById('rq-ws-btn');
|
|
1103
|
+
if (btn) { btn.textContent = '\u5b9e\u65f6'; btn.style.color = 'var(--success)'; }
|
|
1104
|
+
};
|
|
1105
|
+
ws.onmessage = (e) => {
|
|
1106
|
+
try {
|
|
1107
|
+
const msg = JSON.parse(e.data);
|
|
1108
|
+
const entry = msg.id ? msg : (msg.data || msg);
|
|
1109
|
+
if (entry && entry.id) {
|
|
1110
|
+
requestLogs.unshift(entry);
|
|
1111
|
+
if (requestLogs.length > 500) requestLogs.pop();
|
|
1112
|
+
if (currentPage === 'request-logs') {
|
|
1113
|
+
// Prepend single row instead of full re-render to preserve open detail rows
|
|
1114
|
+
const tbody = document.getElementById('rq-tbody');
|
|
1115
|
+
if (tbody) {
|
|
1116
|
+
const proxyFilter = document.getElementById('rq-proxy-filter')?.value || '';
|
|
1117
|
+
const statusFilter = document.getElementById('rq-status-filter')?.value || '';
|
|
1118
|
+
const modelFilter = (document.getElementById('rq-model-filter')?.value || '').toLowerCase();
|
|
1119
|
+
let passes = true;
|
|
1120
|
+
if (proxyFilter && entry.proxyId !== proxyFilter) passes = false;
|
|
1121
|
+
if (statusFilter === 'success' && entry.status !== 'success') passes = false;
|
|
1122
|
+
if (statusFilter === 'failure' && entry.status === 'success') passes = false;
|
|
1123
|
+
if (statusFilter === '429' && entry.status !== '429') passes = false;
|
|
1124
|
+
if (modelFilter && !(entry.model || '').toLowerCase().includes(modelFilter)) passes = false;
|
|
1125
|
+
if (passes) {
|
|
1126
|
+
const tmp = document.createElement('tbody');
|
|
1127
|
+
tmp.innerHTML = renderRequestLogRow(entry);
|
|
1128
|
+
const newRow = tmp.firstElementChild;
|
|
1129
|
+
if (newRow) tbody.insertBefore(newRow, tbody.firstChild);
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
if (currentPage === 'dashboard') renderDashRecentRequests();
|
|
1134
|
+
}
|
|
1135
|
+
} catch {}
|
|
1136
|
+
};
|
|
1137
|
+
ws.onclose = () => {
|
|
1138
|
+
const btn = document.getElementById('rq-ws-btn');
|
|
1139
|
+
if (btn) { btn.textContent = '\u5df2\u65ad\u5f00'; btn.style.color = 'var(--error)'; }
|
|
1140
|
+
setTimeout(connectRequestLogWS, 3000);
|
|
1141
|
+
};
|
|
1142
|
+
ws.onerror = () => {
|
|
1143
|
+
const btn = document.getElementById('rq-ws-btn');
|
|
1144
|
+
if (btn) { btn.textContent = '\u8fde\u63a5\u5931\u8d25'; btn.style.color = 'var(--error)'; }
|
|
1145
|
+
};
|
|
1146
|
+
} catch {
|
|
1147
|
+
// WS not available, show as disabled
|
|
1148
|
+
const btn = document.getElementById('rq-ws-btn');
|
|
1149
|
+
if (btn) { btn.textContent = '\u672a\u8fde\u63a5'; btn.style.color = 'var(--text-faint)'; }
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
function renderRequestLogs() {
|
|
1154
|
+
const tbody = document.getElementById('rq-tbody');
|
|
1155
|
+
if (!tbody) return;
|
|
1156
|
+
|
|
1157
|
+
const proxyFilter = document.getElementById('rq-proxy-filter')?.value || '';
|
|
1158
|
+
const statusFilter = document.getElementById('rq-status-filter')?.value || '';
|
|
1159
|
+
const modelFilter = (document.getElementById('rq-model-filter')?.value || '').toLowerCase();
|
|
1160
|
+
|
|
1161
|
+
const filtered = requestLogs.filter(r => {
|
|
1162
|
+
if (proxyFilter && r.proxyId !== proxyFilter) return false;
|
|
1163
|
+
if (statusFilter) {
|
|
1164
|
+
if (statusFilter === 'success' && r.status !== 'success') return false;
|
|
1165
|
+
if (statusFilter === 'failure' && r.status === 'success') return false;
|
|
1166
|
+
if (statusFilter === '429' && r.status !== '429') return false;
|
|
1167
|
+
}
|
|
1168
|
+
if (modelFilter && !(r.model || '').toLowerCase().includes(modelFilter)) return false;
|
|
1169
|
+
return true;
|
|
1170
|
+
});
|
|
1171
|
+
|
|
1172
|
+
// Update summary
|
|
1173
|
+
const total = filtered.length;
|
|
1174
|
+
const success = filtered.filter(r => r.status === 'success').length;
|
|
1175
|
+
const avgLatency = total > 0 ? Math.round(filtered.reduce((a, r) => a + (r.latency || 0), 0) / total) : 0;
|
|
1176
|
+
const summary = document.getElementById('rq-summary');
|
|
1177
|
+
if (summary) {
|
|
1178
|
+
summary.innerHTML = `
|
|
1179
|
+
<div class="request-log-summary-item">\u603b\u8ba1: <strong>${total}</strong></div>
|
|
1180
|
+
<div class="request-log-summary-item">\u6210\u529f: <strong style="color:var(--success)">${success}</strong></div>
|
|
1181
|
+
<div class="request-log-summary-item">\u5931\u8d25: <strong style="color:var(--error)">${total - success}</strong></div>
|
|
1182
|
+
<div class="request-log-summary-item">\u5e73\u5747\u5ef6\u8fdf: <strong>${avgLatency}ms</strong></div>
|
|
1183
|
+
`;
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
if (filtered.length === 0) {
|
|
1187
|
+
tbody.innerHTML = '<tr><td colspan="9" class="empty-cell">\u6682\u65e0\u8bf7\u6c42\u8bb0\u5f55</td></tr>';
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
|
|
1191
|
+
tbody.innerHTML = filtered.slice(0, 200).map(r => renderRequestLogRow(r)).join('');
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
function toggleRequestLogDetail(row, entry) {
|
|
1195
|
+
const next = row.nextElementSibling;
|
|
1196
|
+
if (next && next.classList.contains('request-log-detail-row')) {
|
|
1197
|
+
next.remove();
|
|
1198
|
+
return;
|
|
1199
|
+
}
|
|
1200
|
+
const detailRow = document.createElement('tr');
|
|
1201
|
+
detailRow.className = 'request-log-detail-row';
|
|
1202
|
+
const time = entry.timestamp ? new Date(entry.timestamp).toLocaleString('zh-CN', { hour12: false }) : '-';
|
|
1203
|
+
detailRow.innerHTML = `<td colspan="9"><div class="request-log-detail">
|
|
1204
|
+
<div class="request-log-detail-grid">
|
|
1205
|
+
<span class="detail-label">Request ID</span><span><code>${escapeHtml(entry.id || '-')}</code></span>
|
|
1206
|
+
<span class="detail-label">时间</span><span>${time}</span>
|
|
1207
|
+
<span class="detail-label">客户端 IP</span><span>${escapeHtml(entry.clientIP || '-')}</span>
|
|
1208
|
+
<span class="detail-label">入站协议</span><span>${escapeHtml(entry.inboundProtocol || '-')}</span>
|
|
1209
|
+
<span class="detail-label">目标协议</span><span>${escapeHtml(entry.targetProtocol || '-')}</span>
|
|
1210
|
+
<span class="detail-label">代理</span><span>${escapeHtml(entry.proxyName || '-')} <code style="font-size:11px;color:var(--text-dim)">${escapeHtml(entry.proxyId || '')}</code></span>
|
|
1211
|
+
<span class="detail-label">供应商</span><span>${escapeHtml(entry.providerName || '-')}</span>
|
|
1212
|
+
<span class="detail-label">模型</span><span><code>${escapeHtml(entry.model || '-')}</code></span>
|
|
1213
|
+
<span class="detail-label">Key</span><span>${escapeHtml(entry.keyAlias || '-')}</span>
|
|
1214
|
+
<span class="detail-label">流式</span><span>${entry.stream ? '是' : '否'}</span>
|
|
1215
|
+
<span class="detail-label">上游状态码</span><span>${entry.upstreamStatusCode || '-'}</span>
|
|
1216
|
+
<span class="detail-label">延迟</span><span>${entry.latencyMs != null ? entry.latencyMs + 'ms' : '-'}</span>
|
|
1217
|
+
<span class="detail-label">Token</span><span>${entry.promptTokens || 0} 输入 + ${entry.completionTokens || 0} 输出 = ${entry.totalTokens || 0} ${entry.isEstimated ? '(估算)' : ''}</span>
|
|
1218
|
+
${entry.errorMessage ? `<span class="detail-label">错误信息</span><span class="request-log-detail-error">${escapeHtml(entry.errorMessage)}</span>` : ''}
|
|
1219
|
+
</div>
|
|
1220
|
+
</div></td>`;
|
|
1221
|
+
row.after(detailRow);
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
function renderRequestLogRow(r, compact) {
|
|
1225
|
+
const time = r.timestamp ? new Date(r.timestamp).toLocaleTimeString('zh-CN', { hour12: false }) : '-';
|
|
1226
|
+
const status = r.status === 'success'
|
|
1227
|
+
? `<span style="color:var(--success);font-weight:600">\u2713</span>`
|
|
1228
|
+
: (r.status === '429'
|
|
1229
|
+
? `<span style="color:var(--warning);font-weight:600">429</span>`
|
|
1230
|
+
: `<span style="color:var(--error);font-weight:600">\u2717</span>`);
|
|
1231
|
+
const latency = r.latencyMs ? `<span style="color:var(--text-muted)">${r.latencyMs}ms</span>` : '-';
|
|
1232
|
+
const tokens = r.totalTokens ? formatTokens(r.totalTokens) : '-';
|
|
1233
|
+
const keyLabel = r.keyAlias || (r.key ? `\u2026${r.key.slice(-4)}` : '-');
|
|
1234
|
+
|
|
1235
|
+
if (compact) {
|
|
1236
|
+
return `
|
|
1237
|
+
<tr>
|
|
1238
|
+
<td>${time}</td>
|
|
1239
|
+
<td>${escapeHtml(r.proxyName || r.proxyId || '-')}</td>
|
|
1240
|
+
<td><code>${escapeHtml(r.protocol || '-')}</code></td>
|
|
1241
|
+
<td><code>${escapeHtml(r.model || '-')}</code></td>
|
|
1242
|
+
<td>${status}</td>
|
|
1243
|
+
<td class="num">${latency}</td>
|
|
1244
|
+
</tr>
|
|
1245
|
+
`;
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
return `
|
|
1249
|
+
<tr class="clickable" data-entry-id="${escapeHtml(r.id || '')}">
|
|
1250
|
+
<td>${time}</td>
|
|
1251
|
+
<td>${escapeHtml(r.proxyName || r.proxyId || '-')}</td>
|
|
1252
|
+
<td><code>${escapeHtml(r.protocol || '-')}</code></td>
|
|
1253
|
+
<td><code>${escapeHtml(r.model || '-')}</code></td>
|
|
1254
|
+
<td>${status}</td>
|
|
1255
|
+
<td class="num">${tokens}</td>
|
|
1256
|
+
<td class="num">${latency}</td>
|
|
1257
|
+
<td>${escapeHtml(r.providerName || r.provider || '-')}</td>
|
|
1258
|
+
<td><span style="font-family:var(--font-mono);font-size:12px;color:var(--text-muted)">${escapeHtml(keyLabel)}</span></td>
|
|
1259
|
+
</tr>
|
|
1260
|
+
`;
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
function filterRequestLogs() {
|
|
1264
|
+
renderRequestLogs();
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
function exportRequestLogs() {
|
|
1268
|
+
if (requestLogs.length === 0) {
|
|
1269
|
+
showToast('\u65e0\u6570\u636e\u53ef\u5bfc\u51fa', true);
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
const rows = [['\u65f6\u95f4', '\u4ee3\u7406', '\u534f\u8bae', '\u6a21\u578b', '\u72b6\u6001', 'Tokens', '\u5ef6\u8fdf', '\u4f9b\u5e94\u5546', 'Key']];
|
|
1273
|
+
for (const r of requestLogs) {
|
|
1274
|
+
rows.push([r.timestamp || '-', r.proxyName || r.proxyId || '-', r.inboundProtocol || '-', r.model || '-', r.status === 'success' ? '\u6210\u529f' : '\u5931\u8d25', r.totalTokens || 0, r.latencyMs || 0, r.providerName || '-', r.keyAlias || '-']);
|
|
1275
|
+
}
|
|
1276
|
+
const csv = '\ufeff' + rows.map(r => r.map(c => `"${String(c).replace(/"/g, '""')}"`).join(',')).join('\n');
|
|
1277
|
+
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8' });
|
|
1278
|
+
const url = URL.createObjectURL(blob);
|
|
1279
|
+
const a = document.createElement('a');
|
|
1280
|
+
a.href = url;
|
|
1281
|
+
a.download = `request-logs-${new Date().toISOString().slice(0,10)}.csv`;
|
|
1282
|
+
a.click();
|
|
1283
|
+
URL.revokeObjectURL(url);
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
function clearRequestLogs() {
|
|
1287
|
+
requestLogs = [];
|
|
1288
|
+
renderRequestLogs();
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
// ---------- System Logs ----------
|
|
1292
|
+
function renderLogs(lines) {
|
|
1293
|
+
const container = document.getElementById('log-content');
|
|
1294
|
+
if (!container) return;
|
|
1295
|
+
if (!lines || lines.length === 0) {
|
|
1296
|
+
container.innerHTML = '<div class="empty-sm">\u6682\u65e0\u65e5\u5fd7</div>';
|
|
1297
|
+
return;
|
|
1298
|
+
}
|
|
1299
|
+
container.innerHTML = lines.map(line => {
|
|
1300
|
+
const levelMatch = line.match(/\[(ERROR|WARN|INFO)\]/i);
|
|
1301
|
+
let levelClass = '';
|
|
1302
|
+
if (levelMatch) {
|
|
1303
|
+
const lvl = levelMatch[1].toUpperCase();
|
|
1304
|
+
if (lvl === 'ERROR') levelClass = 'log-level-error';
|
|
1305
|
+
else if (lvl === 'WARN') levelClass = 'log-level-warn';
|
|
1306
|
+
else levelClass = 'log-level-info';
|
|
1307
|
+
}
|
|
1308
|
+
const timeMatch = line.match(/^(\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}:\d{2})/);
|
|
1309
|
+
let display = escapeHtml(line);
|
|
1310
|
+
if (timeMatch) {
|
|
1311
|
+
display = `<span class="log-time">${escapeHtml(timeMatch[1])}</span>` + escapeHtml(line.slice(timeMatch[1].length));
|
|
1312
|
+
}
|
|
1313
|
+
return `<div class="log-line ${levelClass}">${display}</div>`;
|
|
1314
|
+
}).join('');
|
|
1315
|
+
container.scrollTop = container.scrollHeight;
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1318
|
+
// ---------- Config Import/Export ----------
|
|
1319
|
+
async function exportConfig() {
|
|
1320
|
+
try {
|
|
1321
|
+
const res = await fetch('/api/config/export');
|
|
1322
|
+
const data = await res.json();
|
|
1323
|
+
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
|
1324
|
+
const url = URL.createObjectURL(blob);
|
|
1325
|
+
const a = document.createElement('a');
|
|
1326
|
+
a.href = url;
|
|
1327
|
+
a.download = `config-backup-${new Date().toISOString().slice(0,10).replace(/-/g,'')}.json`;
|
|
1328
|
+
a.click();
|
|
1329
|
+
URL.revokeObjectURL(url);
|
|
1330
|
+
showToast('\u914d\u7f6e\u5df2\u5bfc\u51fa');
|
|
1331
|
+
} catch (err) {
|
|
1332
|
+
showToast('\u5bfc\u51fa\u5931\u8d25: ' + err.message, true);
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
function handleImportFile(e) {
|
|
1337
|
+
const file = e.target.files[0];
|
|
1338
|
+
if (!file) return;
|
|
1339
|
+
e.target.value = '';
|
|
1340
|
+
const reader = new FileReader();
|
|
1341
|
+
reader.onload = () => {
|
|
1342
|
+
try {
|
|
1343
|
+
const data = JSON.parse(reader.result);
|
|
1344
|
+
if (!Array.isArray(data.providers) || !Array.isArray(data.proxies)) {
|
|
1345
|
+
showToast('\u914d\u7f6e\u683c\u5f0f\u9519\u8bef', true);
|
|
1346
|
+
return;
|
|
1347
|
+
}
|
|
1348
|
+
importData = data;
|
|
1349
|
+
document.getElementById('import-providers-count').textContent = data.providers.length;
|
|
1350
|
+
document.getElementById('import-proxies-count').textContent = data.proxies.length;
|
|
1351
|
+
showModal('import-modal');
|
|
1352
|
+
} catch (err) {
|
|
1353
|
+
showToast('\u6587\u4ef6\u89e3\u6790\u5931\u8d25: ' + err.message, true);
|
|
1354
|
+
}
|
|
1355
|
+
};
|
|
1356
|
+
reader.readAsText(file);
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
function closeImportModal() {
|
|
1360
|
+
hideModal('import-modal');
|
|
1361
|
+
importData = null;
|
|
1362
|
+
}
|
|
1363
|
+
|
|
1364
|
+
async function confirmImport() {
|
|
1365
|
+
if (!importData) return;
|
|
1366
|
+
const mode = document.querySelector('input[name="import-mode"]:checked')?.value || 'merge';
|
|
1367
|
+
if (mode === 'overwrite') {
|
|
1368
|
+
const ok = await showConfirm('\u786e\u8ba4<strong>\u8986\u76d6</strong>\u73b0\u6709\u914d\u7f6e\uff1f\u6b64\u64cd\u4f5c\u4e0d\u53ef\u64a4\u9500\u3002');
|
|
1369
|
+
if (!ok) return;
|
|
1370
|
+
}
|
|
1371
|
+
try {
|
|
1372
|
+
const res = await fetch('/api/config/import', {
|
|
1373
|
+
method: 'POST',
|
|
1374
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1375
|
+
body: JSON.stringify({ config: importData, mode }),
|
|
1376
|
+
});
|
|
1377
|
+
const result = await res.json();
|
|
1378
|
+
if (!res.ok) {
|
|
1379
|
+
showToast(result.error || '\u5bfc\u5165\u5931\u8d25', true);
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1382
|
+
closeImportModal();
|
|
1383
|
+
await Promise.all([loadProxies(), loadProviders()]);
|
|
1384
|
+
const added = result.added;
|
|
1385
|
+
let msg = `\u5bfc\u5165\u6210\u529f`;
|
|
1386
|
+
if (added) msg += `\uff1a\u65b0\u589e ${added.providers} \u4f9b\u5e94\u5546\u3001${added.proxies} \u4ee3\u7406`;
|
|
1387
|
+
showToast(msg);
|
|
1388
|
+
const restart = await showConfirm(msg + `\u3002<br><br>\u662f\u5426\u7acb\u5373\u91cd\u542f\u6240\u6709\u4ee3\u7406\uff1f`);
|
|
1389
|
+
if (restart) {
|
|
1390
|
+
await restartAllProxies();
|
|
1391
|
+
}
|
|
1392
|
+
} catch (err) {
|
|
1393
|
+
showToast('\u5bfc\u5165\u5931\u8d25: ' + err.message, true);
|
|
1394
|
+
}
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
async function restartAllProxies() {
|
|
1398
|
+
try {
|
|
1399
|
+
const statusRes = await fetch('/api/status');
|
|
1400
|
+
const status = await statusRes.json();
|
|
1401
|
+
const runningIds = (status.running || []).map(r => r.id);
|
|
1402
|
+
for (const id of runningIds) {
|
|
1403
|
+
await fetch(`/api/proxies/${id}/stop`, { method: 'POST' });
|
|
1404
|
+
}
|
|
1405
|
+
await loadProxies();
|
|
1406
|
+
for (const p of proxies) {
|
|
1407
|
+
await fetch(`/api/proxies/${p.id}/start`, { method: 'POST' });
|
|
1408
|
+
}
|
|
1409
|
+
await loadProxies();
|
|
1410
|
+
showToast('\u6240\u6709\u4ee3\u7406\u5df2\u91cd\u542f');
|
|
1411
|
+
} catch (err) {
|
|
1412
|
+
showToast('\u91cd\u542f\u5931\u8d25: ' + err.message, true);
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
// ---------- History Modal ----------
|
|
1417
|
+
async function openHistoryModal() {
|
|
1418
|
+
try {
|
|
1419
|
+
const res = await fetch('/api/config/history');
|
|
1420
|
+
const data = await res.json();
|
|
1421
|
+
const list = document.getElementById('history-list');
|
|
1422
|
+
const snapshots = data.snapshots || [];
|
|
1423
|
+
if (snapshots.length === 0) {
|
|
1424
|
+
list.innerHTML = '<div class="empty-sm">\u6682\u65e0\u5386\u53f2\u8bb0\u5f55</div>';
|
|
1425
|
+
} else {
|
|
1426
|
+
list.innerHTML = snapshots.map(s => `
|
|
1427
|
+
<div class="history-item">
|
|
1428
|
+
<div class="history-meta">
|
|
1429
|
+
<div class="history-name">${escapeHtml(s.file)}</div>
|
|
1430
|
+
<div class="history-reason">${escapeHtml(s.reason)} \u00b7 ${new Date(s.timestamp).toLocaleString('zh-CN')}</div>
|
|
1431
|
+
</div>
|
|
1432
|
+
<div class="history-actions">
|
|
1433
|
+
<button class="btn btn-sm history-rollback-btn" data-file="${escapeHtml(s.file)}">\u56de\u6eda</button>
|
|
1434
|
+
</div>
|
|
1435
|
+
</div>
|
|
1436
|
+
`).join('');
|
|
1437
|
+
list.querySelectorAll('.history-rollback-btn').forEach(btn => {
|
|
1438
|
+
btn.addEventListener('click', () => rollbackConfig(btn.dataset.file));
|
|
1439
|
+
});
|
|
1440
|
+
}
|
|
1441
|
+
showModal('history-modal');
|
|
1442
|
+
} catch (err) {
|
|
1443
|
+
showToast('\u52a0\u8f7d\u5386\u53f2\u5931\u8d25: ' + err.message, true);
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
function closeHistoryModal() {
|
|
1448
|
+
hideModal('history-modal');
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1451
|
+
async function rollbackConfig(file) {
|
|
1452
|
+
const ok = await showConfirm(`\u786e\u5b9a\u56de\u6eda\u5230 <strong>${escapeHtml(file)}</strong>\uff1f`);
|
|
1453
|
+
if (!ok) return;
|
|
1454
|
+
try {
|
|
1455
|
+
await fetch('/api/config/rollback', {
|
|
1456
|
+
method: 'POST',
|
|
1457
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1458
|
+
body: JSON.stringify({ file }),
|
|
1459
|
+
});
|
|
1460
|
+
closeHistoryModal();
|
|
1461
|
+
await Promise.all([loadProxies(), loadProviders()]);
|
|
1462
|
+
showToast('\u5df2\u56de\u6eda\u5230\u9009\u5b9a\u7248\u672c');
|
|
1463
|
+
} catch (err) {
|
|
1464
|
+
showToast('\u56de\u6eda\u5931\u8d25: ' + err.message, true);
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
// ---------- Confirm Modal ----------
|
|
1469
|
+
let confirmResolve = null;
|
|
1470
|
+
|
|
1471
|
+
function showConfirm(html, okText) {
|
|
1472
|
+
return new Promise(resolve => {
|
|
1473
|
+
confirmResolve = resolve;
|
|
1474
|
+
document.getElementById('confirm-text').innerHTML = html;
|
|
1475
|
+
const okBtn = document.getElementById('confirm-ok');
|
|
1476
|
+
okBtn.textContent = okText || '\u786e\u8ba4';
|
|
1477
|
+
okBtn.onclick = () => { hideModal('confirm-modal'); resolve(true); };
|
|
1478
|
+
document.getElementById('confirm-cancel').onclick = () => { hideModal('confirm-modal'); resolve(false); };
|
|
1479
|
+
showModal('confirm-modal');
|
|
1480
|
+
});
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
// ---------- Modal Helpers ----------
|
|
1484
|
+
function showModal(id) {
|
|
1485
|
+
const overlay = document.getElementById(id);
|
|
1486
|
+
if (overlay) overlay.classList.add('active');
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
function hideModal(id) {
|
|
1490
|
+
const overlay = document.getElementById(id);
|
|
1491
|
+
if (overlay) overlay.classList.remove('active');
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
// Close modal on overlay click
|
|
1495
|
+
document.querySelectorAll('.modal-overlay').forEach(overlay => {
|
|
1496
|
+
overlay.addEventListener('click', (e) => {
|
|
1497
|
+
if (e.target === overlay) overlay.classList.remove('active');
|
|
1498
|
+
});
|
|
1499
|
+
});
|
|
1500
|
+
|
|
1501
|
+
// Close modal on Escape
|
|
1502
|
+
document.addEventListener('keydown', (e) => {
|
|
1503
|
+
if (e.key === 'Escape') {
|
|
1504
|
+
document.querySelectorAll('.modal-overlay.active').forEach(o => o.classList.remove('active'));
|
|
1505
|
+
}
|
|
1506
|
+
});
|
|
1507
|
+
|
|
1508
|
+
// ---------- Toast ----------
|
|
1509
|
+
let toastTimer = null;
|
|
1510
|
+
function showToast(message, isError) {
|
|
1511
|
+
const toast = document.getElementById('toast');
|
|
1512
|
+
toast.textContent = message;
|
|
1513
|
+
toast.className = 'toast' + (isError ? ' error' : '');
|
|
1514
|
+
toast.style.display = 'block';
|
|
1515
|
+
clearTimeout(toastTimer);
|
|
1516
|
+
toastTimer = setTimeout(() => { toast.style.display = 'none'; }, 2800);
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
// ---------- Utilities ----------
|
|
1520
|
+
function escapeHtml(text) {
|
|
1521
|
+
if (text == null) return '';
|
|
1522
|
+
const div = document.createElement('div');
|
|
1523
|
+
div.textContent = String(text);
|
|
1524
|
+
return div.innerHTML;
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
function formatTokens(n) {
|
|
1528
|
+
if (!n || n === 0) return '0';
|
|
1529
|
+
if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
|
|
1530
|
+
if (n >= 1000) return (n / 1000).toFixed(1) + 'K';
|
|
1531
|
+
return n.toLocaleString();
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
|
|
1535
|
+
// ---------- Initialization ----------
|
|
1536
|
+
async function init() {
|
|
1537
|
+
// Set default dates for stats
|
|
1538
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
1539
|
+
const startInput = document.getElementById('stats-start');
|
|
1540
|
+
const endInput = document.getElementById('stats-end');
|
|
1541
|
+
if (startInput && !startInput.value) startInput.value = today;
|
|
1542
|
+
if (endInput && !endInput.value) endInput.value = today;
|
|
1543
|
+
|
|
1544
|
+
// Safe event bindings
|
|
1545
|
+
const proxyAuth = document.getElementById('proxy-auth');
|
|
1546
|
+
if (proxyAuth) proxyAuth.addEventListener('change', function() {
|
|
1547
|
+
document.getElementById('proxy-auth-token-group').style.display = this.value === 'true' ? '' : 'none';
|
|
1548
|
+
});
|
|
1549
|
+
const proxyProvider = document.getElementById('proxy-provider');
|
|
1550
|
+
if (proxyProvider) proxyProvider.addEventListener('change', function() {
|
|
1551
|
+
updateProxyModelSelect(this.value);
|
|
1552
|
+
});
|
|
1553
|
+
const providerProtocol = document.getElementById('provider-protocol');
|
|
1554
|
+
if (providerProtocol) providerProtocol.addEventListener('change', function() {
|
|
1555
|
+
document.getElementById('provider-azure-row').style.display = this.value === 'openai' ? 'grid' : 'none';
|
|
1556
|
+
});
|
|
1557
|
+
|
|
1558
|
+
await Promise.all([loadProxies(), loadProviders(), loadKeyHealth()]);
|
|
1559
|
+
loadStats();
|
|
1560
|
+
loadLogs();
|
|
1561
|
+
loadRequestLogHistory();
|
|
1562
|
+
connectRequestLogWS();
|
|
1563
|
+
refreshDashboard();
|
|
1564
|
+
|
|
1565
|
+
// Request log row click → toggle detail
|
|
1566
|
+
const rqTbody = document.getElementById('rq-tbody');
|
|
1567
|
+
if (rqTbody && !rqTbody._delegated) {
|
|
1568
|
+
rqTbody.addEventListener('click', (ev) => {
|
|
1569
|
+
const row = ev.target.closest('tr.clickable');
|
|
1570
|
+
if (!row || row.parentElement !== rqTbody) return;
|
|
1571
|
+
const entryId = row.dataset.entryId;
|
|
1572
|
+
const entry = requestLogs.find(r => r.id === entryId);
|
|
1573
|
+
if (entry) toggleRequestLogDetail(row, entry);
|
|
1574
|
+
});
|
|
1575
|
+
rqTbody._delegated = true;
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
// Auto-refresh
|
|
1579
|
+
setInterval(loadStats, 30000);
|
|
1580
|
+
setInterval(loadKeyHealth, 5 * 60 * 1000);
|
|
1581
|
+
|
|
1582
|
+
// Assistant textarea auto-resize
|
|
1583
|
+
const assistantInput = document.getElementById('assistant-input');
|
|
1584
|
+
if (assistantInput) {
|
|
1585
|
+
assistantInput.addEventListener('input', function() {
|
|
1586
|
+
this.style.height = 'auto';
|
|
1587
|
+
this.style.height = Math.min(this.scrollHeight, 120) + 'px';
|
|
1588
|
+
});
|
|
1589
|
+
assistantInput.addEventListener('keydown', function(e) {
|
|
1590
|
+
if (e.key === 'Enter' && !e.shiftKey) {
|
|
1591
|
+
e.preventDefault();
|
|
1592
|
+
sendAssistantMessage();
|
|
1593
|
+
}
|
|
1594
|
+
});
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
async function loadRequestLogHistory() {
|
|
1599
|
+
try {
|
|
1600
|
+
const res = await fetch('/api/request-logs?limit=200');
|
|
1601
|
+
const data = await res.json();
|
|
1602
|
+
requestLogs = data.entries || [];
|
|
1603
|
+
if (currentPage === 'request-logs') renderRequestLogs();
|
|
1604
|
+
if (currentPage === 'dashboard') renderDashRecentRequests();
|
|
1605
|
+
} catch (err) {
|
|
1606
|
+
console.error('loadRequestLogHistory error:', err);
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
// ---------- Assistant ----------
|
|
1611
|
+
|
|
1612
|
+
function populateAssistantProxySelect() {
|
|
1613
|
+
const select = document.getElementById('assistant-proxy-select');
|
|
1614
|
+
if (!select) return;
|
|
1615
|
+
const running = proxies.filter(p => p.running);
|
|
1616
|
+
const current = select.value;
|
|
1617
|
+
select.innerHTML = '<option value="">选择后端代理...</option>' +
|
|
1618
|
+
running.map(p => `<option value="${escapeHtml(p.id)}">${escapeHtml(p.name)} (:${p.port})</option>`).join('');
|
|
1619
|
+
if (current && running.find(p => p.id === current)) {
|
|
1620
|
+
select.value = current;
|
|
1621
|
+
} else {
|
|
1622
|
+
assistantProxyId = '';
|
|
1623
|
+
document.getElementById('assistant-send-btn').disabled = true;
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
function buildSystemPrompt() {
|
|
1628
|
+
const now = new Date().toLocaleString('zh-CN', { hour12: false });
|
|
1629
|
+
return `你是 Protocol Proxy 的智能助手,专门帮助管理员监控和排障。当前时间:${now}
|
|
1630
|
+
|
|
1631
|
+
你有以下工具可以调用:
|
|
1632
|
+
|
|
1633
|
+
系统查询:
|
|
1634
|
+
- get_system_status: 获取系统概览(代理运行状态、供应商数量、运行时长)
|
|
1635
|
+
- get_providers / get_provider: 获取供应商列表或详情
|
|
1636
|
+
- get_proxies / get_proxy: 获取代理列表或详情
|
|
1637
|
+
- get_usage_stats: 查询用量统计(支持按时间范围、代理筛选)
|
|
1638
|
+
- get_recent_requests: 获取最近请求日志
|
|
1639
|
+
- get_system_logs: 获取系统日志
|
|
1640
|
+
- get_key_health: 获取 API Key 健康检查结果
|
|
1641
|
+
- get_settings: 获取系统设置项
|
|
1642
|
+
- get_config_history: 获取配置快照历史
|
|
1643
|
+
|
|
1644
|
+
文件与命令:
|
|
1645
|
+
- read_file: 读取任意文件内容(支持指定行范围)
|
|
1646
|
+
- write_file: 写入文件(会覆盖已有内容)
|
|
1647
|
+
- list_directory: 列出目录内容
|
|
1648
|
+
- search_files: 按 glob 模式搜索文件
|
|
1649
|
+
- execute_command: 执行 shell 命令
|
|
1650
|
+
|
|
1651
|
+
规则:
|
|
1652
|
+
- 当用户询问系统状态、代理、供应商、日志、用量等运维相关问题时,调用工具获取实时数据后再回答
|
|
1653
|
+
- 当用户需要查看或修改文件、执行命令时,使用对应的文件和命令工具
|
|
1654
|
+
- 当用户只是打招呼、闲聊、或询问与系统无关的问题时,直接回答,不要调用工具
|
|
1655
|
+
- 不要凭空猜测系统状态,需要数据时必须调用工具
|
|
1656
|
+
- 执行写操作或危险命令前,先告知用户将要做什么
|
|
1657
|
+
|
|
1658
|
+
你的职责:
|
|
1659
|
+
1. 回答关于代理配置和运行状态的问题
|
|
1660
|
+
2. 分析日志,指出异常和可能原因
|
|
1661
|
+
3. 根据数据给出优化建议(负载均衡、模型选择、故障切换策略)
|
|
1662
|
+
4. 用自然语言解释技术问题
|
|
1663
|
+
5. 如果发现问题,给出具体的修复步骤
|
|
1664
|
+
|
|
1665
|
+
请用中文回答,保持专业且易懂。`;
|
|
1666
|
+
}
|
|
1667
|
+
|
|
1668
|
+
async function sendAssistantMessage() {
|
|
1669
|
+
const input = document.getElementById('assistant-input');
|
|
1670
|
+
const text = input.value.trim();
|
|
1671
|
+
if (!text || !assistantProxyId) return;
|
|
1672
|
+
|
|
1673
|
+
addAssistantMessage('user', text);
|
|
1674
|
+
input.value = '';
|
|
1675
|
+
input.style.height = 'auto';
|
|
1676
|
+
|
|
1677
|
+
const systemPrompt = buildSystemPrompt();
|
|
1678
|
+
|
|
1679
|
+
// 构建消息历史,正确序列化 tool 相关字段
|
|
1680
|
+
const messages = [
|
|
1681
|
+
{ role: 'system', content: systemPrompt },
|
|
1682
|
+
...assistantMessages.filter(m => m.role !== 'thinking' && m.role !== 'tool-calls' && m.role !== 'tool-result').map(m => {
|
|
1683
|
+
if (m.role === 'assistant' && m.tool_calls) {
|
|
1684
|
+
const msg = { role: 'assistant', content: m.content || null, tool_calls: m.tool_calls };
|
|
1685
|
+
if (m.reasoning_content) msg.reasoning_content = m.reasoning_content;
|
|
1686
|
+
return msg;
|
|
1687
|
+
}
|
|
1688
|
+
if (m.role === 'assistant' && m.reasoning_content) {
|
|
1689
|
+
const msg = { role: 'assistant', content: m.content };
|
|
1690
|
+
msg.reasoning_content = m.reasoning_content;
|
|
1691
|
+
return msg;
|
|
1692
|
+
}
|
|
1693
|
+
if (m.role === 'tool') {
|
|
1694
|
+
return { role: 'tool', tool_call_id: m.tool_call_id, content: m.content };
|
|
1695
|
+
}
|
|
1696
|
+
return { role: m.role, content: m.content };
|
|
1697
|
+
})
|
|
1698
|
+
];
|
|
1699
|
+
|
|
1700
|
+
const proxy = proxies.find(p => p.id === assistantProxyId);
|
|
1701
|
+
if (!proxy) {
|
|
1702
|
+
addAssistantMessage('assistant', '所选代理不存在或已停止,请重新选择。');
|
|
1703
|
+
return;
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
const thinkingId = addAssistantMessage('thinking', '');
|
|
1707
|
+
assistantAbortController = new AbortController();
|
|
1708
|
+
|
|
1709
|
+
try {
|
|
1710
|
+
const res = await fetch('/api/assistant/chat', {
|
|
1711
|
+
method: 'POST',
|
|
1712
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1713
|
+
body: JSON.stringify({ proxyId: proxy.id, messages }),
|
|
1714
|
+
signal: assistantAbortController.signal,
|
|
1715
|
+
});
|
|
1716
|
+
|
|
1717
|
+
if (!res.ok) {
|
|
1718
|
+
const err = await res.text().catch(() => 'Unknown error');
|
|
1719
|
+
removeAssistantMessage(thinkingId);
|
|
1720
|
+
addAssistantMessage('assistant', `请求失败: HTTP ${res.status}\n\n${err}`);
|
|
1721
|
+
return;
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
const reader = res.body.getReader();
|
|
1725
|
+
const decoder = new TextDecoder();
|
|
1726
|
+
let buffer = '';
|
|
1727
|
+
let fullContent = '';
|
|
1728
|
+
let currentEvent = '';
|
|
1729
|
+
let msgId = null;
|
|
1730
|
+
|
|
1731
|
+
removeAssistantMessage(thinkingId);
|
|
1732
|
+
console.log('[assistant] SSE stream started');
|
|
1733
|
+
|
|
1734
|
+
while (true) {
|
|
1735
|
+
const { done, value } = await reader.read();
|
|
1736
|
+
if (done) break;
|
|
1737
|
+
|
|
1738
|
+
buffer += decoder.decode(value, { stream: true });
|
|
1739
|
+
const lines = buffer.split('\n');
|
|
1740
|
+
buffer = lines.pop();
|
|
1741
|
+
|
|
1742
|
+
for (const line of lines) {
|
|
1743
|
+
const trimmed = line.trim();
|
|
1744
|
+
if (!trimmed) continue;
|
|
1745
|
+
|
|
1746
|
+
if (trimmed.startsWith('event: ')) {
|
|
1747
|
+
currentEvent = trimmed.slice(7);
|
|
1748
|
+
continue;
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
if (!trimmed.startsWith('data: ')) continue;
|
|
1752
|
+
let data;
|
|
1753
|
+
try { data = JSON.parse(trimmed.slice(6)); } catch { continue; }
|
|
1754
|
+
|
|
1755
|
+
console.log('[assistant] SSE event:', currentEvent, data);
|
|
1756
|
+
switch (currentEvent) {
|
|
1757
|
+
case 'content': {
|
|
1758
|
+
if (!msgId) msgId = addAssistantMessage('assistant', '');
|
|
1759
|
+
fullContent += data.delta;
|
|
1760
|
+
updateAssistantMessage(msgId, fullContent);
|
|
1761
|
+
break;
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
case 'tool_calls': {
|
|
1765
|
+
const calls = data.calls || [];
|
|
1766
|
+
const toolCallsData = calls.map(tc => ({
|
|
1767
|
+
id: tc.id,
|
|
1768
|
+
type: 'function',
|
|
1769
|
+
function: { name: tc.name, arguments: JSON.stringify(tc.arguments || {}) },
|
|
1770
|
+
}));
|
|
1771
|
+
// 存储 assistant 消息(含 tool_calls)用于对话序列化
|
|
1772
|
+
const assistantMsg = {
|
|
1773
|
+
id: 'msg-' + Date.now() + '-' + Math.random().toString(36).slice(2),
|
|
1774
|
+
role: 'assistant',
|
|
1775
|
+
content: fullContent || null,
|
|
1776
|
+
tool_calls: toolCallsData,
|
|
1777
|
+
};
|
|
1778
|
+
if (data.reasoning_content) assistantMsg.reasoning_content = data.reasoning_content;
|
|
1779
|
+
assistantMessages.push(assistantMsg);
|
|
1780
|
+
fullContent = '';
|
|
1781
|
+
// 创建显示用的 tool-calls 消息
|
|
1782
|
+
const callHtml = calls.map(tc => {
|
|
1783
|
+
const argsStr = Object.keys(tc.arguments || {}).length > 0
|
|
1784
|
+
? `<span class="tool-call-args">${escapeHtml(JSON.stringify(tc.arguments))}</span>`
|
|
1785
|
+
: '';
|
|
1786
|
+
return `<div class="tool-call-item"><span class="tool-call-name">${escapeHtml(tc.name)}</span>${argsStr}</div>`;
|
|
1787
|
+
}).join('');
|
|
1788
|
+
addAssistantMessage('tool-calls', callHtml);
|
|
1789
|
+
break;
|
|
1790
|
+
}
|
|
1791
|
+
|
|
1792
|
+
case 'tool_result': {
|
|
1793
|
+
const resultStr = JSON.stringify(data.result, null, 2);
|
|
1794
|
+
addAssistantMessage('tool-result', { name: data.name, result: resultStr, tool_call_id: data.tool_call_id });
|
|
1795
|
+
// 追加 role: 'tool' 消息用于对话序列化(不创建 DOM 元素)
|
|
1796
|
+
assistantMessages.push({
|
|
1797
|
+
id: 'msg-' + Date.now() + '-' + Math.random().toString(36).slice(2),
|
|
1798
|
+
role: 'tool',
|
|
1799
|
+
tool_call_id: data.tool_call_id,
|
|
1800
|
+
tool_name: data.name,
|
|
1801
|
+
content: JSON.stringify(data.result),
|
|
1802
|
+
});
|
|
1803
|
+
break;
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
case 'done': {
|
|
1807
|
+
if (msgId) {
|
|
1808
|
+
const msgObj = assistantMessages.find(m => m.id === msgId);
|
|
1809
|
+
if (msgObj) {
|
|
1810
|
+
msgObj.content = fullContent;
|
|
1811
|
+
if (data.reasoning_content) msgObj.reasoning_content = data.reasoning_content;
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
break;
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
case 'error': {
|
|
1818
|
+
addAssistantMessage('assistant', `错误: ${data.message}`);
|
|
1819
|
+
break;
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
// 流结束但没有收到 done 事件时,确保最终内容被保存
|
|
1826
|
+
if (msgId && fullContent) {
|
|
1827
|
+
const msgObj = assistantMessages.find(m => m.id === msgId);
|
|
1828
|
+
if (msgObj && !msgObj.content) msgObj.content = fullContent;
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
} catch (err) {
|
|
1832
|
+
removeAssistantMessage(thinkingId);
|
|
1833
|
+
if (err.name === 'AbortError') {
|
|
1834
|
+
addAssistantMessage('assistant', '已取消');
|
|
1835
|
+
} else {
|
|
1836
|
+
addAssistantMessage('assistant', `请求出错: ${err.message}`);
|
|
1837
|
+
}
|
|
1838
|
+
} finally {
|
|
1839
|
+
assistantAbortController = null;
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
|
|
1843
|
+
function addAssistantMessage(role, content) {
|
|
1844
|
+
const id = 'msg-' + Date.now() + '-' + Math.random().toString(36).slice(2);
|
|
1845
|
+
assistantMessages.push({ id, role, content });
|
|
1846
|
+
|
|
1847
|
+
const chat = document.getElementById('assistant-chat');
|
|
1848
|
+
if (!chat) return id;
|
|
1849
|
+
|
|
1850
|
+
const displayRoles = ['user', 'assistant', 'tool', 'tool-calls', 'tool-result'];
|
|
1851
|
+
if (assistantMessages.filter(m => displayRoles.includes(m.role)).length === 1 && role === 'user') {
|
|
1852
|
+
chat.innerHTML = '';
|
|
1853
|
+
}
|
|
1854
|
+
|
|
1855
|
+
const div = document.createElement('div');
|
|
1856
|
+
div.id = id;
|
|
1857
|
+
div.className = `assistant-message ${role}`;
|
|
1858
|
+
|
|
1859
|
+
if (role === 'thinking') {
|
|
1860
|
+
div.innerHTML = `<div class="assistant-dot"></div><div class="assistant-dot"></div><div class="assistant-dot"></div>`;
|
|
1861
|
+
} else if (role === 'tool-calls') {
|
|
1862
|
+
// content 已经是 HTML 字符串
|
|
1863
|
+
div.innerHTML = `<div class="tool-calls-header">调用工具</div>${content}`;
|
|
1864
|
+
} else if (role === 'tool-result') {
|
|
1865
|
+
// content 是 {name, result, tool_call_id}
|
|
1866
|
+
const toolData = typeof content === 'object' ? content : { name: 'unknown', result: content };
|
|
1867
|
+
const resultId = 'result-' + id;
|
|
1868
|
+
div.innerHTML = `
|
|
1869
|
+
<div class="tool-result-header" onclick="document.getElementById('${resultId}').classList.toggle('expanded')">
|
|
1870
|
+
<span class="tool-result-name">${escapeHtml(toolData.name)}</span>
|
|
1871
|
+
<span class="tool-result-toggle">展开结果 ▾</span>
|
|
1872
|
+
</div>
|
|
1873
|
+
<div class="tool-result-body" id="${resultId}">
|
|
1874
|
+
<pre>${escapeHtml(toolData.result)}</pre>
|
|
1875
|
+
</div>`;
|
|
1876
|
+
// 保存 tool_call_id 到消息对象
|
|
1877
|
+
const msgObj = assistantMessages.find(m => m.id === id);
|
|
1878
|
+
if (msgObj) {
|
|
1879
|
+
msgObj.tool_call_id = toolData.tool_call_id;
|
|
1880
|
+
msgObj.tool_name = toolData.name;
|
|
1881
|
+
msgObj.content = toolData.result;
|
|
1882
|
+
}
|
|
1883
|
+
} else if (role === 'assistant') {
|
|
1884
|
+
div.innerHTML = formatAssistantContent(content);
|
|
1885
|
+
} else {
|
|
1886
|
+
div.textContent = content;
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
chat.appendChild(div);
|
|
1890
|
+
chat.scrollTop = chat.scrollHeight;
|
|
1891
|
+
return id;
|
|
1892
|
+
}
|
|
1893
|
+
|
|
1894
|
+
function updateAssistantMessage(id, content) {
|
|
1895
|
+
const div = document.getElementById(id);
|
|
1896
|
+
if (div) {
|
|
1897
|
+
div.innerHTML = formatAssistantContent(content);
|
|
1898
|
+
const chat = document.getElementById('assistant-chat');
|
|
1899
|
+
if (chat) chat.scrollTop = chat.scrollHeight;
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
function removeAssistantMessage(id) {
|
|
1904
|
+
const div = document.getElementById(id);
|
|
1905
|
+
if (div) div.remove();
|
|
1906
|
+
assistantMessages = assistantMessages.filter(m => m.id !== id);
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
function formatAssistantContent(text) {
|
|
1910
|
+
if (!text) return '';
|
|
1911
|
+
let html = escapeHtml(text);
|
|
1912
|
+
// 代码块
|
|
1913
|
+
html = html.replace(/```([\s\S]*?)```/g, '<pre><code>$1</code></pre>');
|
|
1914
|
+
// 行内代码
|
|
1915
|
+
html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
|
|
1916
|
+
// 段落
|
|
1917
|
+
const paragraphs = html.split(/\n{2,}/);
|
|
1918
|
+
html = paragraphs.map(p => {
|
|
1919
|
+
p = p.trim();
|
|
1920
|
+
if (!p) return '';
|
|
1921
|
+
// 如果已经是 pre,不包 p
|
|
1922
|
+
if (p.startsWith('<pre>')) return p;
|
|
1923
|
+
// 列表项
|
|
1924
|
+
if (p.startsWith('- ') || p.startsWith('* ')) {
|
|
1925
|
+
const items = p.split('\n').filter(l => l.trim().startsWith('- ') || l.trim().startsWith('* '));
|
|
1926
|
+
return '<ul>' + items.map(i => `<li>${i.trim().slice(2)}</li>`).join('') + '</ul>';
|
|
1927
|
+
}
|
|
1928
|
+
// 数字列表
|
|
1929
|
+
if (/^\d+\./.test(p)) {
|
|
1930
|
+
const items = p.split('\n').filter(l => /^\d+\./.test(l.trim()));
|
|
1931
|
+
return '<ol>' + items.map(i => `<li>${i.trim().replace(/^\d+\.\s*/, '')}</li>`).join('') + '</ol>';
|
|
1932
|
+
}
|
|
1933
|
+
return '<p>' + p.replace(/\n/g, '<br>') + '</p>';
|
|
1934
|
+
}).join('');
|
|
1935
|
+
return html;
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
function clearAssistantChat() {
|
|
1939
|
+
assistantMessages = [];
|
|
1940
|
+
const chat = document.getElementById('assistant-chat');
|
|
1941
|
+
if (!chat) return;
|
|
1942
|
+
chat.innerHTML = `
|
|
1943
|
+
<div class="assistant-welcome">
|
|
1944
|
+
<div class="assistant-welcome-icon">
|
|
1945
|
+
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path></svg>
|
|
1946
|
+
</div>
|
|
1947
|
+
<h3>智控助手</h3>
|
|
1948
|
+
<p>我是你的 Protocol Proxy 智能助手,可以帮你:</p>
|
|
1949
|
+
<ul>
|
|
1950
|
+
<li>查询代理和供应商运行状态</li>
|
|
1951
|
+
<li>分析日志,定位异常原因</li>
|
|
1952
|
+
<li>解读配置并给出优化建议</li>
|
|
1953
|
+
<li>自然语言排障与问答</li>
|
|
1954
|
+
</ul>
|
|
1955
|
+
<p class="assistant-hint">请先选择一个运行中的代理作为对话后端</p>
|
|
1956
|
+
</div>
|
|
1957
|
+
`;
|
|
1958
|
+
}
|
|
1959
|
+
|
|
1960
|
+
// 监听代理选择
|
|
1961
|
+
(function() {
|
|
1962
|
+
const select = document.getElementById('assistant-proxy-select');
|
|
1963
|
+
if (select) {
|
|
1964
|
+
select.addEventListener('change', function() {
|
|
1965
|
+
assistantProxyId = this.value;
|
|
1966
|
+
const btn = document.getElementById('assistant-send-btn');
|
|
1967
|
+
if (btn) btn.disabled = !this.value;
|
|
1968
|
+
});
|
|
1969
|
+
}
|
|
1970
|
+
})();
|
|
1971
|
+
|
|
1972
|
+
init();
|