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