ai-zero-token 1.0.7 → 1.0.9

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.
@@ -27,6 +27,9 @@ function renderAdminPage() {
27
27
  --orange-soft: rgba(245, 158, 11, 0.12);
28
28
  --red: #ef4444;
29
29
  --red-soft: rgba(239, 68, 68, 0.12);
30
+ --plan-color: #94a3b8;
31
+ --plan-soft: rgba(148, 163, 184, 0.12);
32
+ --plan-border: var(--line);
30
33
  --shadow: 0 2px 10px rgba(15, 23, 42, 0.06);
31
34
  --radius: 16px;
32
35
  --radius-sm: 12px;
@@ -341,6 +344,45 @@ function renderAdminPage() {
341
344
  min-height: 112px;
342
345
  }
343
346
 
347
+ .summary-card-head {
348
+ display: flex;
349
+ align-items: center;
350
+ gap: 8px;
351
+ min-width: 0;
352
+ }
353
+
354
+ .summary-icon {
355
+ width: 22px;
356
+ height: 22px;
357
+ border-radius: 999px;
358
+ display: inline-grid;
359
+ place-items: center;
360
+ flex: 0 0 auto;
361
+ color: var(--brand);
362
+ background: var(--brand-soft);
363
+ }
364
+
365
+ .summary-icon svg {
366
+ width: 14px;
367
+ height: 14px;
368
+ display: block;
369
+ }
370
+
371
+ .summary-icon.blue {
372
+ color: var(--blue);
373
+ background: var(--blue-soft);
374
+ }
375
+
376
+ .summary-icon.green {
377
+ color: #15803d;
378
+ background: var(--green-soft);
379
+ }
380
+
381
+ .summary-icon.orange {
382
+ color: #b45309;
383
+ background: var(--orange-soft);
384
+ }
385
+
344
386
  .summary-card label {
345
387
  color: var(--text-muted);
346
388
  font-size: 12px;
@@ -368,6 +410,60 @@ function renderAdminPage() {
368
410
  overflow-wrap: anywhere;
369
411
  }
370
412
 
413
+ .summary-card.account-status-summary {
414
+ gap: 12px;
415
+ min-height: 112px;
416
+ }
417
+
418
+ .account-status-list {
419
+ display: grid;
420
+ gap: 8px;
421
+ }
422
+
423
+ .account-status-line {
424
+ display: grid;
425
+ grid-template-columns: 20px auto minmax(0, 1fr);
426
+ align-items: center;
427
+ gap: 8px;
428
+ min-width: 0;
429
+ color: var(--text-soft);
430
+ font-size: 13px;
431
+ line-height: 1.45;
432
+ }
433
+
434
+ .account-status-line svg {
435
+ width: 18px;
436
+ height: 18px;
437
+ padding: 3px;
438
+ border-radius: 999px;
439
+ flex: 0 0 auto;
440
+ }
441
+
442
+ .account-status-line.gateway svg {
443
+ color: var(--blue);
444
+ background: var(--blue-soft);
445
+ }
446
+
447
+ .account-status-line.codex svg {
448
+ color: #15803d;
449
+ background: var(--green-soft);
450
+ }
451
+
452
+ .account-status-line span {
453
+ color: var(--text-muted);
454
+ font-weight: 700;
455
+ white-space: nowrap;
456
+ }
457
+
458
+ .account-status-line strong {
459
+ min-width: 0;
460
+ color: var(--text);
461
+ font-size: clamp(16px, 1.05vw, 18px);
462
+ line-height: 1.25;
463
+ letter-spacing: -0.02em;
464
+ overflow-wrap: anywhere;
465
+ }
466
+
371
467
  .main-grid {
372
468
  display: grid;
373
469
  grid-template-columns: minmax(0, 1fr) minmax(460px, 560px);
@@ -487,6 +583,88 @@ function renderAdminPage() {
487
583
  display: flex;
488
584
  }
489
585
 
586
+ .drawer-backdrop {
587
+ position: fixed;
588
+ inset: 0;
589
+ display: none;
590
+ justify-content: flex-end;
591
+ background: rgba(15, 23, 42, 0.38);
592
+ backdrop-filter: blur(4px);
593
+ z-index: 60;
594
+ }
595
+
596
+ .drawer-backdrop.is-open {
597
+ display: flex;
598
+ }
599
+
600
+ .settings-drawer {
601
+ width: min(460px, 100vw);
602
+ height: 100vh;
603
+ background: var(--panel);
604
+ border-left: 1px solid var(--line);
605
+ box-shadow: -18px 0 48px rgba(15, 23, 42, 0.16);
606
+ display: grid;
607
+ grid-template-rows: auto minmax(0, 1fr) auto;
608
+ }
609
+
610
+ .settings-drawer-head,
611
+ .settings-drawer-footer {
612
+ padding: 18px 20px;
613
+ display: flex;
614
+ align-items: flex-start;
615
+ justify-content: space-between;
616
+ gap: 14px;
617
+ border-bottom: 1px solid var(--line);
618
+ }
619
+
620
+ .settings-drawer-footer {
621
+ align-items: center;
622
+ border-top: 1px solid var(--line);
623
+ border-bottom: 0;
624
+ background: #fbfdff;
625
+ }
626
+
627
+ .settings-drawer-head h3 {
628
+ margin: 0;
629
+ font-size: 22px;
630
+ line-height: 1.2;
631
+ letter-spacing: -0.03em;
632
+ }
633
+
634
+ .settings-drawer-head p {
635
+ margin: 6px 0 0;
636
+ color: var(--text-muted);
637
+ font-size: 13px;
638
+ line-height: 1.6;
639
+ }
640
+
641
+ .settings-drawer-body {
642
+ padding: 18px 20px;
643
+ overflow: auto;
644
+ display: grid;
645
+ align-content: start;
646
+ gap: 16px;
647
+ }
648
+
649
+ .settings-section {
650
+ display: grid;
651
+ gap: 12px;
652
+ padding-bottom: 16px;
653
+ border-bottom: 1px solid var(--line);
654
+ }
655
+
656
+ .settings-section:last-child {
657
+ border-bottom: 0;
658
+ padding-bottom: 0;
659
+ }
660
+
661
+ .settings-section h4 {
662
+ margin: 0;
663
+ color: var(--text);
664
+ font-size: 14px;
665
+ line-height: 1.4;
666
+ }
667
+
490
668
  .modal-card {
491
669
  width: min(760px, calc(100vw - 32px));
492
670
  background: var(--panel);
@@ -662,35 +840,89 @@ function renderAdminPage() {
662
840
 
663
841
  .account-grid {
664
842
  display: grid;
665
- grid-auto-rows: 1fr;
666
843
  gap: 16px;
667
- justify-content: start;
844
+ align-items: start;
845
+ justify-content: stretch;
846
+ width: 100%;
668
847
  }
669
848
 
670
849
  .account-grid.profile-count-1 {
671
- grid-template-columns: minmax(300px, 440px);
850
+ grid-template-columns: minmax(340px, 520px);
672
851
  }
673
852
 
674
853
  .account-grid.profile-count-2 {
675
- grid-template-columns: repeat(2, minmax(280px, 360px));
854
+ grid-template-columns: repeat(2, minmax(340px, 1fr));
676
855
  }
677
856
 
678
857
  .account-grid.profile-count-3 {
679
- grid-template-columns: repeat(3, minmax(260px, 320px));
858
+ grid-template-columns: repeat(3, minmax(320px, 1fr));
680
859
  }
681
860
 
682
861
  .account-grid.profile-count-many {
683
- grid-template-columns: repeat(auto-fit, minmax(250px, 300px));
862
+ grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
684
863
  }
685
864
 
686
865
  .account-card {
866
+ --plan-color: #94a3b8;
867
+ --plan-soft: rgba(148, 163, 184, 0.12);
868
+ --plan-border: var(--line);
869
+ --usage-color: #16a34a;
870
+ --usage-soft: rgba(22, 163, 74, 0.12);
871
+ position: relative;
687
872
  border-radius: 16px;
688
873
  padding: 14px;
689
874
  display: grid;
690
875
  grid-template-rows: auto auto auto 1fr auto;
691
876
  gap: 12px;
692
877
  min-width: 0;
693
- height: 100%;
878
+ border-color: var(--plan-border);
879
+ overflow: hidden;
880
+ }
881
+
882
+ .account-card::before {
883
+ content: "";
884
+ position: absolute;
885
+ inset: 0 0 auto;
886
+ height: 3px;
887
+ background: var(--plan-color);
888
+ }
889
+
890
+ .account-card.plan-free {
891
+ --plan-color: #94a3b8;
892
+ --plan-soft: rgba(148, 163, 184, 0.12);
893
+ --plan-border: var(--line);
894
+ }
895
+
896
+ .account-card.plan-plus {
897
+ --plan-color: #635bff;
898
+ --plan-soft: rgba(99, 91, 255, 0.11);
899
+ --plan-border: rgba(99, 91, 255, 0.2);
900
+ }
901
+
902
+ .account-card.plan-pro {
903
+ --plan-color: #4f46e5;
904
+ --plan-soft: rgba(79, 70, 229, 0.11);
905
+ --plan-border: rgba(79, 70, 229, 0.22);
906
+ }
907
+
908
+ .account-card.plan-team {
909
+ --plan-color: #0f766e;
910
+ --plan-soft: rgba(15, 118, 110, 0.11);
911
+ --plan-border: rgba(15, 118, 110, 0.22);
912
+ }
913
+
914
+ .account-card.plan-premium {
915
+ --plan-color: #d97706;
916
+ --plan-soft: rgba(217, 119, 6, 0.12);
917
+ --plan-border: rgba(217, 119, 6, 0.34);
918
+ box-shadow: 0 10px 26px rgba(180, 83, 9, 0.1), var(--shadow);
919
+ }
920
+
921
+ .account-card.plan-enterprise {
922
+ --plan-color: #a16207;
923
+ --plan-soft: rgba(161, 98, 7, 0.14);
924
+ --plan-border: rgba(71, 85, 105, 0.28);
925
+ box-shadow: 0 12px 28px rgba(15, 23, 42, 0.1), var(--shadow);
694
926
  }
695
927
 
696
928
  .account-head {
@@ -698,6 +930,7 @@ function renderAdminPage() {
698
930
  align-items: flex-start;
699
931
  justify-content: space-between;
700
932
  gap: 12px;
933
+ padding-top: 8px;
701
934
  }
702
935
 
703
936
  .account-title {
@@ -713,6 +946,7 @@ function renderAdminPage() {
713
946
  gap: 6px;
714
947
  min-height: 28px;
715
948
  padding: 0 8px;
949
+ margin-top: 28px;
716
950
  border: 1px solid var(--line);
717
951
  border-radius: 8px;
718
952
  background: #fff;
@@ -742,11 +976,12 @@ function renderAdminPage() {
742
976
  height: 24px;
743
977
  border-radius: 999px;
744
978
  background: var(--panel-soft);
745
- border: 1px solid var(--line);
979
+ border: 1px solid var(--plan-color);
980
+ box-shadow: 0 0 0 3px var(--plan-soft);
746
981
  display: grid;
747
982
  place-items: center;
748
983
  font-size: 11px;
749
- color: var(--brand);
984
+ color: var(--plan-color);
750
985
  font-weight: 700;
751
986
  flex: 0 0 auto;
752
987
  }
@@ -783,8 +1018,66 @@ function renderAdminPage() {
783
1018
  }
784
1019
 
785
1020
  .badge.brand {
786
- color: var(--brand);
787
- background: var(--brand-soft);
1021
+ color: var(--plan-color);
1022
+ background: var(--plan-soft);
1023
+ }
1024
+
1025
+ .usage-corner {
1026
+ position: absolute;
1027
+ top: 10px;
1028
+ right: 12px;
1029
+ min-height: 24px;
1030
+ padding: 0 10px 0 8px;
1031
+ border-radius: 999px;
1032
+ color: #047857;
1033
+ background: linear-gradient(135deg, #ecfdf5, #d1fae5);
1034
+ border: 1px solid rgba(16, 185, 129, 0.28);
1035
+ box-shadow: 0 8px 18px rgba(16, 185, 129, 0.12), inset 0 1px 0 rgba(255, 255, 255, 0.82);
1036
+ font-size: 10px;
1037
+ font-weight: 800;
1038
+ line-height: 22px;
1039
+ letter-spacing: 0;
1040
+ pointer-events: none;
1041
+ z-index: 1;
1042
+ display: inline-flex;
1043
+ align-items: center;
1044
+ gap: 5px;
1045
+ }
1046
+
1047
+ .usage-corner::before {
1048
+ content: "";
1049
+ width: 6px;
1050
+ height: 6px;
1051
+ border-radius: 999px;
1052
+ background: currentColor;
1053
+ box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.12);
1054
+ flex: 0 0 auto;
1055
+ }
1056
+
1057
+ .usage-corner span {
1058
+ line-height: 1;
1059
+ }
1060
+
1061
+ .usage-corner.codex-only {
1062
+ color: #1d4ed8;
1063
+ background: linear-gradient(135deg, #eff6ff, #dbeafe);
1064
+ border-color: rgba(37, 99, 235, 0.24);
1065
+ box-shadow: 0 8px 18px rgba(37, 99, 235, 0.12), inset 0 1px 0 rgba(255, 255, 255, 0.82);
1066
+ }
1067
+
1068
+ .usage-corner.codex-only::before {
1069
+ box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.12);
1070
+ }
1071
+
1072
+ .usage-corner.dual {
1073
+ color: #4f46e5;
1074
+ background: linear-gradient(135deg, #f5f3ff, #ede9fe);
1075
+ border-color: rgba(99, 91, 255, 0.25);
1076
+ box-shadow: 0 8px 18px rgba(99, 91, 255, 0.13), inset 0 1px 0 rgba(255, 255, 255, 0.82);
1077
+ }
1078
+
1079
+ .usage-corner.dual::before {
1080
+ box-shadow: 0 0 0 3px rgba(99, 91, 255, 0.12);
788
1081
  }
789
1082
 
790
1083
  .badge.blue {
@@ -812,6 +1105,145 @@ function renderAdminPage() {
812
1105
  gap: 10px;
813
1106
  }
814
1107
 
1108
+ .usage-status-row {
1109
+ display: flex;
1110
+ flex-wrap: nowrap;
1111
+ align-items: center;
1112
+ justify-content: space-between;
1113
+ gap: 8px;
1114
+ padding: 8px 10px;
1115
+ border-radius: 10px;
1116
+ background: var(--panel-soft);
1117
+ color: var(--text-muted);
1118
+ font-size: 11px;
1119
+ line-height: 1.4;
1120
+ }
1121
+
1122
+ .usage-status {
1123
+ display: inline-flex;
1124
+ align-items: center;
1125
+ gap: 5px;
1126
+ min-width: 0;
1127
+ white-space: nowrap;
1128
+ font-weight: 700;
1129
+ }
1130
+
1131
+ .usage-status svg {
1132
+ width: 12px;
1133
+ height: 12px;
1134
+ color: var(--text-muted);
1135
+ flex: 0 0 auto;
1136
+ }
1137
+
1138
+ .usage-dot {
1139
+ width: 6px;
1140
+ height: 6px;
1141
+ border-radius: 999px;
1142
+ background: #cbd5e1;
1143
+ flex: 0 0 auto;
1144
+ }
1145
+
1146
+ .usage-dot.active {
1147
+ background: #22c55e;
1148
+ box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.12);
1149
+ }
1150
+
1151
+ .usage-state-text {
1152
+ color: var(--text-muted);
1153
+ font-weight: 700;
1154
+ }
1155
+
1156
+ .usage-status.is-active .usage-state-text {
1157
+ color: #15803d;
1158
+ }
1159
+
1160
+ .compact-meta-row {
1161
+ display: grid;
1162
+ gap: 8px;
1163
+ min-width: 0;
1164
+ color: var(--text-muted);
1165
+ font-size: 11px;
1166
+ line-height: 1.45;
1167
+ }
1168
+
1169
+ .compact-reset-list {
1170
+ display: flex;
1171
+ flex-wrap: nowrap;
1172
+ align-items: center;
1173
+ gap: 10px;
1174
+ min-width: 0;
1175
+ }
1176
+
1177
+ .compact-meta-item {
1178
+ display: flex;
1179
+ align-items: baseline;
1180
+ gap: 5px;
1181
+ min-width: 0;
1182
+ flex: 1 1 0;
1183
+ }
1184
+
1185
+ .compact-meta-item label {
1186
+ color: var(--text-muted);
1187
+ font-size: 10px;
1188
+ line-height: 1.4;
1189
+ white-space: nowrap;
1190
+ }
1191
+
1192
+ .compact-meta-item strong {
1193
+ color: var(--text-soft);
1194
+ font-size: 11px;
1195
+ line-height: 1.4;
1196
+ text-align: left;
1197
+ overflow-wrap: anywhere;
1198
+ }
1199
+
1200
+ .compact-meta-actions {
1201
+ display: flex;
1202
+ align-items: center;
1203
+ justify-content: center;
1204
+ gap: 10px;
1205
+ margin-top: 2px;
1206
+ }
1207
+
1208
+ .compact-meta-actions::before,
1209
+ .compact-meta-actions::after {
1210
+ content: "";
1211
+ height: 1px;
1212
+ background: var(--line);
1213
+ flex: 1 1 auto;
1214
+ min-width: 18px;
1215
+ }
1216
+
1217
+ .details-toggle {
1218
+ display: inline-flex;
1219
+ align-items: center;
1220
+ justify-content: center;
1221
+ gap: 5px;
1222
+ min-height: 24px;
1223
+ padding: 0 6px;
1224
+ border: 0;
1225
+ background: transparent;
1226
+ color: var(--brand);
1227
+ font-size: 11px;
1228
+ font-weight: 700;
1229
+ white-space: nowrap;
1230
+ cursor: pointer;
1231
+ }
1232
+
1233
+ .details-toggle:hover {
1234
+ color: #4338ca;
1235
+ }
1236
+
1237
+ .details-toggle svg {
1238
+ width: 12px;
1239
+ height: 12px;
1240
+ transition: transform 0.16s ease;
1241
+ }
1242
+
1243
+ .details-toggle.is-expanded svg {
1244
+ transform: rotate(180deg);
1245
+ }
1246
+
815
1247
  .quota-row {
816
1248
  display: grid;
817
1249
  gap: 6px;
@@ -871,6 +1303,8 @@ function renderAdminPage() {
871
1303
  display: grid;
872
1304
  grid-template-columns: repeat(2, minmax(0, 1fr));
873
1305
  gap: 8px 12px;
1306
+ padding-top: 10px;
1307
+ border-top: 1px solid var(--line);
874
1308
  }
875
1309
 
876
1310
  .meta-item {
@@ -911,6 +1345,38 @@ function renderAdminPage() {
911
1345
  font-size: 12px;
912
1346
  }
913
1347
 
1348
+ .account-actions .btn-secondary.is-current {
1349
+ position: relative;
1350
+ opacity: 1;
1351
+ color: #047857;
1352
+ background: linear-gradient(135deg, #f0fdf4, #dcfce7);
1353
+ border-color: rgba(16, 185, 129, 0.36);
1354
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.8), 0 6px 14px rgba(16, 185, 129, 0.08);
1355
+ cursor: default;
1356
+ }
1357
+
1358
+ .account-actions .btn-secondary.is-current::before {
1359
+ content: "";
1360
+ width: 7px;
1361
+ height: 7px;
1362
+ border-radius: 999px;
1363
+ background: #22c55e;
1364
+ box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.12);
1365
+ flex: 0 0 auto;
1366
+ }
1367
+
1368
+ .account-actions .btn-secondary.is-current.codex {
1369
+ color: #1d4ed8;
1370
+ background: linear-gradient(135deg, #eff6ff, #dbeafe);
1371
+ border-color: rgba(37, 99, 235, 0.32);
1372
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.8), 0 6px 14px rgba(37, 99, 235, 0.08);
1373
+ }
1374
+
1375
+ .account-actions .btn-secondary.is-current.codex::before {
1376
+ background: #3b82f6;
1377
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.12);
1378
+ }
1379
+
914
1380
  .account-status {
915
1381
  display: inline-flex;
916
1382
  align-items: center;
@@ -1325,6 +1791,10 @@ function renderAdminPage() {
1325
1791
  padding: 12px;
1326
1792
  }
1327
1793
 
1794
+ .settings-drawer {
1795
+ width: 100vw;
1796
+ }
1797
+
1328
1798
  .modal-head,
1329
1799
  .modal-body {
1330
1800
  padding-left: 16px;
@@ -1366,7 +1836,7 @@ function renderAdminPage() {
1366
1836
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><path d="M8 6h13"></path><path d="M8 12h13"></path><path d="M8 18h13"></path><path d="M3 6h.01"></path><path d="M3 12h.01"></path><path d="M3 18h.01"></path></svg>
1367
1837
  \u8BF7\u6C42\u65E5\u5FD7
1368
1838
  </button>
1369
- <button class="nav-item" type="button" data-nav-target="settings">
1839
+ <button class="nav-item" type="button" data-open-settings>
1370
1840
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8"><circle cx="12" cy="12" r="3"></circle><path d="M19.4 15a1.6 1.6 0 0 0 .33 1.76l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.6 1.6 0 0 0-1.76-.33 1.6 1.6 0 0 0-.97 1.46V21a2 2 0 0 1-4 0v-.09a1.6 1.6 0 0 0-.97-1.46 1.6 1.6 0 0 0-1.76.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.6 1.6 0 0 0 .33-1.76 1.6 1.6 0 0 0-1.46-.97H3a2 2 0 0 1 0-4h.09a1.6 1.6 0 0 0 1.46-.97 1.6 1.6 0 0 0-.33-1.76l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.6 1.6 0 0 0 1.76.33H9a1.6 1.6 0 0 0 .97-1.46V3a2 2 0 0 1 4 0v.09a1.6 1.6 0 0 0 .97 1.46 1.6 1.6 0 0 0 1.76-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.6 1.6 0 0 0-.33 1.76V9c0 .64.38 1.22.97 1.46H21a2 2 0 0 1 0 4h-.09c-.64 0-1.22.38-1.46.97Z"></path></svg>
1371
1841
  \u7CFB\u7EDF\u8BBE\u7F6E
1372
1842
  </button>
@@ -1389,6 +1859,7 @@ function renderAdminPage() {
1389
1859
  </div>
1390
1860
  <div class="top-actions">
1391
1861
  <a class="btn-link" href="https://github.com/fchangjun/AI-Zero-Token" target="_blank" rel="noreferrer">GitHub \u4ED3\u5E93</a>
1862
+ <button class="btn-secondary" id="openSettingsBtn" type="button">\u8BBE\u7F6E</button>
1392
1863
  <button class="btn-secondary" id="contactBtn" type="button">\u4EA4\u6D41\u53CD\u9988</button>
1393
1864
  <button class="btn-secondary" id="toggleEmailBtn" type="button">\u8131\u654F\u6A21\u5F0F</button>
1394
1865
  <button class="btn-primary" id="loginBtn" type="button">+ \u65B0\u589E\u8D26\u53F7</button>
@@ -1428,7 +1899,7 @@ function renderAdminPage() {
1428
1899
  <option value="healthy">\u5065\u5EB7</option>
1429
1900
  <option value="warning">\u5373\u5C06\u8017\u5C3D</option>
1430
1901
  <option value="expired">\u5DF2\u8FC7\u671F</option>
1431
- <option value="active">\u5F53\u524D\u4F7F\u7528</option>
1902
+ <option value="active">\u4F7F\u7528\u4E2D</option>
1432
1903
  </select>
1433
1904
  <select class="control" id="profileSort">
1434
1905
  <option value="quota-desc">\u6309\u4E3B\u989D\u5EA6\u6392\u5E8F</option>
@@ -1508,32 +1979,6 @@ function renderAdminPage() {
1508
1979
  <select class="control" id="endpointSelect"></select>
1509
1980
  </div>
1510
1981
 
1511
- <div class="field" id="settings">
1512
- <label for="defaultModel">\u9ED8\u8BA4\u6A21\u578B</label>
1513
- <select class="control" id="defaultModel"></select>
1514
- <p class="hint">\u5207\u6362\u540E\u4F1A\u5F71\u54CD\u672A\u663E\u5F0F\u4F20 <code>model</code> \u7684\u8BF7\u6C42\u3002</p>
1515
- <p class="hint" id="modelCatalogHint"></p>
1516
- <div class="actions">
1517
- <button class="btn-secondary" id="refreshModelsBtn" type="button">\u540C\u6B65 Codex \u6A21\u578B</button>
1518
- <button class="btn-primary" id="saveModelBtn" type="button">\u4FDD\u5B58\u9ED8\u8BA4\u6A21\u578B</button>
1519
- </div>
1520
- </div>
1521
-
1522
- <div class="field">
1523
- <label class="checkbox-row" for="proxyEnabled">
1524
- <input id="proxyEnabled" type="checkbox" />
1525
- \u542F\u7528\u4E0A\u6E38\u4EE3\u7406
1526
- </label>
1527
- <label for="proxyUrl">\u4EE3\u7406\u5730\u5740</label>
1528
- <input class="input" id="proxyUrl" type="text" placeholder="\u586B\u5199\u4F60\u7684\u4EE3\u7406\u5730\u5740" />
1529
- <label for="proxyNoProxy">\u76F4\u8FDE\u5730\u5740</label>
1530
- <input class="input" id="proxyNoProxy" type="text" placeholder="localhost,127.0.0.1,::1" />
1531
- <p class="hint">\u542F\u7528\u540E\uFF0COAuth \u6362\u53D6 token\u3001\u6A21\u578B\u5237\u65B0\u548C\u63A5\u53E3\u8F6C\u53D1\u4F1A\u901A\u8FC7\u6B64\u4EE3\u7406\u8BBF\u95EE\u6D77\u5916\u4E0A\u6E38\u3002</p>
1532
- <div class="actions">
1533
- <button class="btn-primary" id="saveProxyBtn" type="button">\u4FDD\u5B58\u4EE3\u7406\u914D\u7F6E</button>
1534
- </div>
1535
- </div>
1536
-
1537
1982
  <div class="field">
1538
1983
  <label for="requestBody">\u8BF7\u6C42\u4F53 JSON</label>
1539
1984
  <textarea class="textarea" id="requestBody" spellcheck="false"></textarea>
@@ -1597,6 +2042,65 @@ function renderAdminPage() {
1597
2042
  </main>
1598
2043
  </div>
1599
2044
 
2045
+ <div class="drawer-backdrop" id="settingsDrawerBackdrop" aria-hidden="true">
2046
+ <aside class="settings-drawer" role="dialog" aria-modal="true" aria-labelledby="settingsDrawerTitle">
2047
+ <div class="settings-drawer-head">
2048
+ <div>
2049
+ <h3 id="settingsDrawerTitle">\u7CFB\u7EDF\u8BBE\u7F6E</h3>
2050
+ <p>\u96C6\u4E2D\u7BA1\u7406\u9ED8\u8BA4\u6A21\u578B\u3001\u4E0A\u6E38\u4EE3\u7406\u548C\u989D\u5EA6\u8017\u5C3D\u540E\u7684\u81EA\u52A8\u5207\u6362\u7B56\u7565\u3002</p>
2051
+ </div>
2052
+ <button class="btn-secondary" id="closeSettingsDrawerBtn" type="button">\u5173\u95ED</button>
2053
+ </div>
2054
+ <div class="settings-drawer-body">
2055
+ <section class="settings-section">
2056
+ <h4>\u9ED8\u8BA4\u6A21\u578B</h4>
2057
+ <div class="field">
2058
+ <label for="defaultModel">\u9ED8\u8BA4\u6A21\u578B</label>
2059
+ <select class="control" id="defaultModel"></select>
2060
+ <p class="hint">\u5F71\u54CD\u672A\u663E\u5F0F\u4F20 <code>model</code> \u7684\u8BF7\u6C42\u3002</p>
2061
+ <p class="hint" id="modelCatalogHint"></p>
2062
+ <div class="actions">
2063
+ <button class="btn-secondary" id="refreshModelsBtn" type="button">\u540C\u6B65 Codex \u6A21\u578B</button>
2064
+ </div>
2065
+ </div>
2066
+ </section>
2067
+
2068
+ <section class="settings-section">
2069
+ <h4>\u4E0A\u6E38\u4EE3\u7406</h4>
2070
+ <div class="field">
2071
+ <label class="checkbox-row" for="proxyEnabled">
2072
+ <input id="proxyEnabled" type="checkbox" />
2073
+ \u542F\u7528\u4E0A\u6E38\u4EE3\u7406
2074
+ </label>
2075
+ <label for="proxyUrl">\u4EE3\u7406\u5730\u5740</label>
2076
+ <input class="input" id="proxyUrl" type="text" placeholder="\u586B\u5199\u4F60\u7684\u4EE3\u7406\u5730\u5740" />
2077
+ <label for="proxyNoProxy">\u76F4\u8FDE\u5730\u5740</label>
2078
+ <input class="input" id="proxyNoProxy" type="text" placeholder="localhost,127.0.0.1,::1" />
2079
+ <p class="hint">\u542F\u7528\u540E\uFF0COAuth \u6362\u53D6 token\u3001\u6A21\u578B\u5237\u65B0\u548C\u63A5\u53E3\u8F6C\u53D1\u4F1A\u901A\u8FC7\u6B64\u4EE3\u7406\u8BBF\u95EE\u6D77\u5916\u4E0A\u6E38\u3002</p>
2080
+ <div class="actions">
2081
+ <button class="btn-secondary" id="testProxyBtn" type="button">\u6D4B\u8BD5\u4EE3\u7406</button>
2082
+ </div>
2083
+ </div>
2084
+ </section>
2085
+
2086
+ <section class="settings-section">
2087
+ <h4>\u8D26\u53F7\u5207\u6362</h4>
2088
+ <div class="field">
2089
+ <label class="checkbox-row" for="autoSwitchEnabled">
2090
+ <input id="autoSwitchEnabled" type="checkbox" />
2091
+ \u989D\u5EA6\u8017\u5C3D\u81EA\u52A8\u5207\u6362
2092
+ </label>
2093
+ <p class="hint">\u5F00\u542F\u540E\uFF0C\u5F53\u524D API \u8D26\u53F7\u989D\u5EA6\u5FEB\u7167\u5DF2\u8017\u5C3D\u65F6\uFF0C\u7F51\u5173\u4F1A\u6309\u8D26\u53F7\u6C60\u987A\u5E8F\u5207\u5230\u4ECD\u6709\u989D\u5EA6\u7684\u8D26\u53F7\uFF0C\u5E76\u5C3D\u91CF\u907F\u5F00 Codex \u6B63\u5728\u4F7F\u7528\u7684\u8D26\u53F7\u3002</p>
2094
+ </div>
2095
+ </section>
2096
+ </div>
2097
+ <div class="settings-drawer-footer">
2098
+ <p class="status-inline" id="settingsStatus"></p>
2099
+ <button class="btn-primary" id="saveSettingsBtn" type="button">\u4FDD\u5B58\u8BBE\u7F6E</button>
2100
+ </div>
2101
+ </aside>
2102
+ </div>
2103
+
1600
2104
  <div class="modal-backdrop" id="imagePreviewModal" aria-hidden="true">
1601
2105
  <section class="modal-card preview-modal-card" role="dialog" aria-modal="true" aria-labelledby="imagePreviewTitle">
1602
2106
  <div class="modal-head">
@@ -1686,7 +2190,8 @@ function renderAdminPage() {
1686
2190
  </div>
1687
2191
 
1688
2192
  <script>
1689
- const RUNTIME_AUTO_REFRESH_MS = 10 * 60 * 1000;
2193
+ const RUNTIME_AUTO_REFRESH_MS = 5 * 60 * 1000;
2194
+ const ACTIVE_PROFILE_REFRESH_MS = 15 * 1000;
1690
2195
 
1691
2196
  const state = {
1692
2197
  config: null,
@@ -1698,7 +2203,9 @@ function renderAdminPage() {
1698
2203
  sort: "quota-desc",
1699
2204
  },
1700
2205
  selectedProfileIds: {},
2206
+ expandedProfileIds: {},
1701
2207
  testerResultTab: "response",
2208
+ settingsDirty: false,
1702
2209
  };
1703
2210
 
1704
2211
  const endpointMeta = {
@@ -1743,6 +2250,7 @@ function renderAdminPage() {
1743
2250
  const accountModal = document.getElementById("accountModal");
1744
2251
  const contactModal = document.getElementById("contactModal");
1745
2252
  const imagePreviewModal = document.getElementById("imagePreviewModal");
2253
+ const settingsDrawerBackdrop = document.getElementById("settingsDrawerBackdrop");
1746
2254
  const contactBtn = document.getElementById("contactBtn");
1747
2255
  const previewModalImage = document.getElementById("previewModalImage");
1748
2256
  const previewModalMeta = document.getElementById("previewModalMeta");
@@ -1759,6 +2267,10 @@ function renderAdminPage() {
1759
2267
  const proxyEnabled = document.getElementById("proxyEnabled");
1760
2268
  const proxyUrl = document.getElementById("proxyUrl");
1761
2269
  const proxyNoProxy = document.getElementById("proxyNoProxy");
2270
+ const testProxyBtn = document.getElementById("testProxyBtn");
2271
+ const autoSwitchEnabled = document.getElementById("autoSwitchEnabled");
2272
+ const settingsStatus = document.getElementById("settingsStatus");
2273
+ const saveSettingsBtn = document.getElementById("saveSettingsBtn");
1762
2274
 
1763
2275
  function setBusy(button, busy) {
1764
2276
  if (button) {
@@ -1850,6 +2362,9 @@ function renderAdminPage() {
1850
2362
  if (minutes === 60 * 24) {
1851
2363
  return "\u65E5\u989D\u5EA6";
1852
2364
  }
2365
+ if (minutes === 60 * 5) {
2366
+ return "5 \u5C0F\u65F6\u989D\u5EA6";
2367
+ }
1853
2368
  if (minutes === 60 * 24 * 7) {
1854
2369
  return "\u5468\u989D\u5EA6";
1855
2370
  }
@@ -1862,6 +2377,68 @@ function renderAdminPage() {
1862
2377
  : "unknown";
1863
2378
  }
1864
2379
 
2380
+ function getPlanRank(profile) {
2381
+ const plan = getPlanType(profile).toLowerCase();
2382
+ if (plan.indexOf("enterprise") !== -1 || plan.indexOf("business") !== -1) {
2383
+ return 60;
2384
+ }
2385
+ if (plan.indexOf("team") !== -1) {
2386
+ return 50;
2387
+ }
2388
+ if (plan.indexOf("pro") !== -1 || plan.indexOf("premium") !== -1) {
2389
+ return 40;
2390
+ }
2391
+ if (plan.indexOf("plus") !== -1) {
2392
+ return 30;
2393
+ }
2394
+ if (plan.indexOf("free") !== -1) {
2395
+ return 10;
2396
+ }
2397
+ return 0;
2398
+ }
2399
+
2400
+ function getPlanKey(profile) {
2401
+ const plan = getPlanType(profile).toLowerCase();
2402
+ if (plan.indexOf("enterprise") !== -1 || plan.indexOf("business") !== -1) {
2403
+ return "enterprise";
2404
+ }
2405
+ if (plan.indexOf("team") !== -1) {
2406
+ return "team";
2407
+ }
2408
+ if (plan.indexOf("pro") !== -1 || plan.indexOf("premium") !== -1) {
2409
+ return plan.indexOf("premium") !== -1 ? "premium" : "pro";
2410
+ }
2411
+ if (plan.indexOf("plus") !== -1) {
2412
+ return "plus";
2413
+ }
2414
+ if (plan.indexOf("free") !== -1) {
2415
+ return "free";
2416
+ }
2417
+ return "unknown";
2418
+ }
2419
+
2420
+ function getUsageCorner(profile, isCodexActive) {
2421
+ if (profile.isActive && isCodexActive) {
2422
+ return {
2423
+ className: "dual",
2424
+ label: "API + Codex",
2425
+ };
2426
+ }
2427
+ if (profile.isActive) {
2428
+ return {
2429
+ className: "api-only",
2430
+ label: "API",
2431
+ };
2432
+ }
2433
+ if (isCodexActive) {
2434
+ return {
2435
+ className: "codex-only",
2436
+ label: "Codex",
2437
+ };
2438
+ }
2439
+ return null;
2440
+ }
2441
+
1865
2442
  function getQuotaSnapshotTime(profile) {
1866
2443
  return profile && profile.quota && typeof profile.quota.capturedAt === "number"
1867
2444
  ? profile.quota.capturedAt
@@ -1933,12 +2510,12 @@ function renderAdminPage() {
1933
2510
  title.textContent = "\u53D1\u73B0\u65B0\u7248\u672C\u53EF\u66F4\u65B0";
1934
2511
  detail.textContent = "\u5F53\u524D\u7248\u672C " + versionStatus.currentVersion + "\uFF0C\u6700\u65B0\u7248\u672C "
1935
2512
  + versionStatus.latestVersion + "\u3002\u66F4\u65B0\u540E\u53EF\u83B7\u5F97\u6700\u65B0\u6A21\u578B\u5217\u8868\u903B\u8F91\u3001\u7BA1\u7406\u9875\u4F53\u9A8C\u548C\u63A5\u53E3\u4FEE\u590D\u3002";
1936
- command.textContent = "npm install -g " + versionStatus.packageName + "@latest";
2513
+ command.textContent = "npm install -g " + versionStatus.packageName;
1937
2514
  panel.classList.add("is-visible");
1938
2515
  }
1939
2516
 
1940
2517
  function supportsImageGeneration(profile) {
1941
- return getPlanType(profile) !== "free";
2518
+ return Boolean(profile);
1942
2519
  }
1943
2520
 
1944
2521
  function getImageCapability(profile) {
@@ -1954,10 +2531,10 @@ function renderAdminPage() {
1954
2531
  const planType = getPlanType(profile);
1955
2532
  if (planType === "free") {
1956
2533
  return {
1957
- supported: false,
1958
- label: "\u751F\u56FE\u53D7\u9650",
1959
- detail: "free \u5957\u9910\u8D26\u53F7\u4E0D\u652F\u6301\u56FE\u7247\u751F\u6210\uFF0C\u8BF7\u5207\u6362\u5230 Plus \u6216\u66F4\u9AD8\u5957\u9910\u8D26\u53F7\u3002",
1960
- badgeClass: "red",
2534
+ supported: true,
2535
+ label: "\u53EF\u5C1D\u8BD5\u751F\u56FE",
2536
+ detail: "free \u8D26\u53F7\u53EF\u5C1D\u8BD5\u56FE\u7247\u751F\u6210\uFF0C\u989D\u5EA6\u548C\u53EF\u7528\u6027\u4EE5\u4E0A\u6E38\u8FD4\u56DE\u4E3A\u51C6\u3002",
2537
+ badgeClass: "orange",
1961
2538
  };
1962
2539
  }
1963
2540
 
@@ -2050,6 +2627,41 @@ function renderAdminPage() {
2050
2627
  return Math.max(0, Math.min(100, value));
2051
2628
  }
2052
2629
 
2630
+ function isProfileQuotaExhausted(profile) {
2631
+ if (!profile || !profile.quota) {
2632
+ return false;
2633
+ }
2634
+
2635
+ return getPrimaryUsage(profile) >= 100 || getSecondaryUsage(profile) >= 100;
2636
+ }
2637
+
2638
+ function findProfileById(profileId) {
2639
+ const profiles = state.config && Array.isArray(state.config.profiles) ? state.config.profiles : [];
2640
+ return profiles.find(function (profile) {
2641
+ return profile.profileId === profileId;
2642
+ }) || null;
2643
+ }
2644
+
2645
+ function confirmQuotaSwitch(action, profileId) {
2646
+ if (action !== "activate" && action !== "apply-codex") {
2647
+ return true;
2648
+ }
2649
+
2650
+ const profile = findProfileById(profileId);
2651
+ if (!isProfileQuotaExhausted(profile)) {
2652
+ return true;
2653
+ }
2654
+
2655
+ const target = action === "activate" ? "\u7F51\u5173" : "Codex";
2656
+ const label = getProfileDisplayLabel(profile);
2657
+ const message = "\u8D26\u53F7 \u201C" + label + "\u201D \u7684\u989D\u5EA6\u5FEB\u7167\u663E\u793A\u5DF2\u8017\u5C3D\u3002\\n\\n\u4ECD\u8981\u5E94\u7528\u5230 " + target + " \u5417\uFF1F";
2658
+ const confirmed = window.confirm(message);
2659
+ if (!confirmed) {
2660
+ authStatus.textContent = "\u5DF2\u53D6\u6D88\u5E94\u7528\u5230 " + target + "\u3002";
2661
+ }
2662
+ return confirmed;
2663
+ }
2664
+
2053
2665
  function getProfileHealth(profile) {
2054
2666
  const now = Date.now();
2055
2667
  if (profile && profile.expiresAt && profile.expiresAt <= now) {
@@ -2105,6 +2717,37 @@ function renderAdminPage() {
2105
2717
  return "\u672A\u77E5";
2106
2718
  }
2107
2719
 
2720
+ function formatCompactDateTime(value) {
2721
+ if (!value) {
2722
+ return "\u6682\u65E0\u6570\u636E";
2723
+ }
2724
+ const date = new Date(value);
2725
+ if (Number.isNaN(date.getTime())) {
2726
+ return "\u672A\u77E5";
2727
+ }
2728
+ const month = String(date.getMonth() + 1);
2729
+ const day = String(date.getDate());
2730
+ const time = date.toLocaleTimeString("zh-CN", { hour12: false, hour: "2-digit", minute: "2-digit" });
2731
+ return month + "/" + day + " " + time;
2732
+ }
2733
+
2734
+ function describeCompactReset(profile, slot) {
2735
+ if (!profile || !profile.quota) {
2736
+ return "\u6682\u65E0\u6570\u636E";
2737
+ }
2738
+
2739
+ const quota = profile.quota;
2740
+ const resetAt = slot === "primary" ? quota.primaryResetAt : quota.secondaryResetAt;
2741
+ const resetAfter = slot === "primary" ? quota.primaryResetAfterSeconds : quota.secondaryResetAfterSeconds;
2742
+ if (typeof resetAt === "number" && resetAt > 0) {
2743
+ return formatCompactDateTime(resetAt * 1000);
2744
+ }
2745
+ if (typeof resetAfter === "number" && resetAfter > 0) {
2746
+ return formatCompactDuration(resetAfter) + "\u540E";
2747
+ }
2748
+ return "\u672A\u77E5";
2749
+ }
2750
+
2108
2751
  function getQuotaWindowLabel(profile, slot) {
2109
2752
  const quota = profile && profile.quota ? profile.quota : null;
2110
2753
  if (!quota) {
@@ -2114,6 +2757,17 @@ function renderAdminPage() {
2114
2757
  return formatWindowLabel(quota && quota[field]);
2115
2758
  }
2116
2759
 
2760
+ function getResetLabel(profile, slot) {
2761
+ const label = getQuotaWindowLabel(profile, slot);
2762
+ if (label === "5 \u5C0F\u65F6\u989D\u5EA6") {
2763
+ return "5\u5C0F\u65F6\u91CD\u7F6E";
2764
+ }
2765
+ if (label === "\u5468\u989D\u5EA6") {
2766
+ return "\u5468\u91CD\u7F6E";
2767
+ }
2768
+ return label.replace("\u989D\u5EA6", "") + "\u91CD\u7F6E";
2769
+ }
2770
+
2117
2771
  function formatQuotaUsage(percent, profile, slot) {
2118
2772
  if (!profile || !profile.quota) {
2119
2773
  return "\u7B49\u5F85\u5237\u65B0";
@@ -2362,50 +3016,58 @@ function renderAdminPage() {
2362
3016
  const codexProfile = codexAccountId && Array.isArray(config.profiles)
2363
3017
  ? config.profiles.find(function (profile) { return profile.accountId === codexAccountId; })
2364
3018
  : null;
3019
+ const gatewayLabel = config.profile ? getProfileDisplayLabel(config.profile) : "\u672A\u6FC0\u6D3B\u8D26\u53F7";
3020
+ const codexLabel = codexProfile
3021
+ ? getProfileDisplayLabel(codexProfile)
3022
+ : (codexAccountId ? maskIdentifier(codexAccountId) : "\u672A\u68C0\u6D4B\u5230");
2365
3023
 
2366
3024
  return [
2367
3025
  {
3026
+ icon: "users",
3027
+ iconClass: "blue",
2368
3028
  label: "\u8D26\u53F7\u603B\u6570",
2369
3029
  value: String(config.status.profileCount || 0),
2370
3030
  detail: "\u5DF2\u4FDD\u5B58\u5230\u672C\u5730\u8D26\u53F7\u6C60",
2371
3031
  },
2372
3032
  {
2373
- label: "\u5F53\u524D\u4F7F\u7528\u8D26\u53F7",
2374
- value: getProfileDisplayLabel(config.profile),
2375
- detail: config.profile && config.profile.profileId
2376
- ? "Profile ID: " + config.profile.profileId + " \xB7 " + getPlanType(config.profile)
2377
- : "\u5C1A\u672A\u6FC0\u6D3B\u8D26\u53F7",
2378
- compact: true,
3033
+ kind: "account-status",
3034
+ label: "\u5F53\u524D\u8D26\u53F7\u72B6\u6001",
3035
+ gatewayLabel: gatewayLabel,
3036
+ codexLabel: codexLabel,
2379
3037
  },
2380
3038
  {
3039
+ icon: "model",
3040
+ iconClass: "brand",
2381
3041
  label: "\u9ED8\u8BA4\u6A21\u578B",
2382
3042
  value: config.settings.defaultModel || "-",
2383
3043
  detail: "\u672A\u663E\u5F0F\u6307\u5B9A model \u65F6\u751F\u6548",
2384
3044
  compact: true,
2385
3045
  },
2386
3046
  {
2387
- label: "Codex \u5F53\u524D\u8D26\u53F7",
2388
- value: codexProfile ? getProfileDisplayLabel(codexProfile) : (codexAccountId ? maskIdentifier(codexAccountId) : "\u672A\u68C0\u6D4B\u5230"),
2389
- detail: config.codex && config.codex.exists ? "\u6765\u81EA ~/.codex/auth.json" : "\u5C1A\u672A\u5E94\u7528\u5230 Codex",
2390
- compact: true,
2391
- },
2392
- {
3047
+ icon: "version",
3048
+ iconClass: config.versionStatus && config.versionStatus.needsUpdate ? "orange" : "green",
2393
3049
  label: "\u5F53\u524D\u7248\u672C",
2394
3050
  value: getVersionValue(config),
2395
3051
  detail: getVersionDetail(config),
2396
3052
  compact: true,
2397
3053
  },
2398
3054
  {
3055
+ icon: "requests",
3056
+ iconClass: "blue",
2399
3057
  label: "\u4ECA\u65E5\u8BF7\u6C42\u6570",
2400
3058
  value: String(requests.length),
2401
3059
  detail: "\u57FA\u4E8E\u672C\u9875\u6700\u8FD1\u6D4B\u8BD5\u8BB0\u5F55",
2402
3060
  },
2403
3061
  {
3062
+ icon: "latency",
3063
+ iconClass: "orange",
2404
3064
  label: "\u5E73\u5747\u8017\u65F6",
2405
3065
  value: requests.length ? (avg / 1000).toFixed(2) + " s" : "--",
2406
3066
  detail: requests.length ? "\u7EDF\u8BA1\u6700\u8FD1 " + String(requests.length) + " \u6B21" : "\u7B49\u5F85\u8BF7\u6C42\u6837\u672C",
2407
3067
  },
2408
3068
  {
3069
+ icon: "service",
3070
+ iconClass: config.status.loggedIn ? "green" : "orange",
2409
3071
  label: "\u670D\u52A1\u72B6\u6001",
2410
3072
  value: config.status.loggedIn ? "\u8FD0\u884C\u4E2D" : "\u5F85\u767B\u5F55",
2411
3073
  detail: config.status.loggedIn ? "\u7F51\u5173\u53EF\u8F6C\u53D1\u8BF7\u6C42" : "\u8BF7\u5148\u5B8C\u6210 OAuth \u767B\u5F55",
@@ -2414,14 +3076,63 @@ function renderAdminPage() {
2414
3076
  ];
2415
3077
  }
2416
3078
 
3079
+ function getSummaryIcon(name) {
3080
+ if (name === "users") {
3081
+ return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"></path><circle cx="9" cy="7" r="4"></circle><path d="M22 21v-2a4 4 0 0 0-3-3.87"></path><path d="M16 3.13a4 4 0 0 1 0 7.75"></path></svg>';
3082
+ }
3083
+ if (name === "model") {
3084
+ return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="4" y="4" width="16" height="16" rx="2"></rect><path d="M9 9h6v6H9z"></path><path d="M9 1v3"></path><path d="M15 1v3"></path><path d="M9 20v3"></path><path d="M15 20v3"></path><path d="M20 9h3"></path><path d="M20 15h3"></path><path d="M1 9h3"></path><path d="M1 15h3"></path></svg>';
3085
+ }
3086
+ if (name === "version") {
3087
+ return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 6 9 17l-5-5"></path></svg>';
3088
+ }
3089
+ if (name === "requests") {
3090
+ return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 12h18"></path><path d="m15 6 6 6-6 6"></path></svg>';
3091
+ }
3092
+ if (name === "latency") {
3093
+ return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 6v6l4 2"></path><circle cx="12" cy="12" r="9"></circle></svg>';
3094
+ }
3095
+ if (name === "service") {
3096
+ return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 3 4 7v6c0 5 3.4 7.7 8 8 4.6-.3 8-3 8-8V7l-8-4Z"></path><path d="m9 12 2 2 4-4"></path></svg>';
3097
+ }
3098
+ return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"></circle></svg>';
3099
+ }
3100
+
2417
3101
  function renderOverview(config) {
2418
3102
  const container = document.getElementById("summaryGrid");
2419
3103
  const cards = getOverviewCards(config);
2420
3104
  container.innerHTML = cards.map(function (card) {
3105
+ if (card.kind === "account-status") {
3106
+ return ""
3107
+ + '<article class="summary-card account-status-summary">'
3108
+ + '<div class="summary-card-head">'
3109
+ + '<span class="summary-icon green">'
3110
+ + '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 17 10 11 4 5"></path><path d="M12 19h8"></path></svg>'
3111
+ + "</span>"
3112
+ + "<label>" + escapeHtml(card.label) + "</label>"
3113
+ + "</div>"
3114
+ + '<div class="account-status-list">'
3115
+ + '<div class="account-status-line gateway">'
3116
+ + '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"></circle><path d="M3 12h18"></path><path d="M12 3c2.5 2.7 3.8 5.7 3.8 9S14.5 18.3 12 21"></path><path d="M12 3c-2.5 2.7-3.8 5.7-3.8 9s1.3 6.3 3.8 9"></path></svg>'
3117
+ + "<span>\u7F51\u5173\uFF1A</span>"
3118
+ + "<strong>" + escapeHtml(card.gatewayLabel) + "</strong>"
3119
+ + "</div>"
3120
+ + '<div class="account-status-line codex">'
3121
+ + '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 17 10 11 4 5"></path><path d="M12 19h8"></path></svg>'
3122
+ + "<span>Codex\uFF1A</span>"
3123
+ + "<strong>" + escapeHtml(card.codexLabel) + "</strong>"
3124
+ + "</div>"
3125
+ + "</div>"
3126
+ + "</article>";
3127
+ }
2421
3128
  const valueClass = card.compact ? "summary-value-sm" : "";
3129
+ const iconClass = card.iconClass ? " " + card.iconClass : "";
2422
3130
  return ""
2423
3131
  + '<article class="summary-card">'
2424
- + "<label>" + escapeHtml(card.label) + "</label>"
3132
+ + '<div class="summary-card-head">'
3133
+ + '<span class="summary-icon' + iconClass + '">' + getSummaryIcon(card.icon) + "</span>"
3134
+ + "<label>" + escapeHtml(card.label) + "</label>"
3135
+ + "</div>"
2425
3136
  + '<strong class="' + valueClass + '">' + escapeHtml(card.value) + "</strong>"
2426
3137
  + "<span>" + escapeHtml(card.detail) + "</span>"
2427
3138
  + "</article>";
@@ -2433,6 +3144,7 @@ function renderAdminPage() {
2433
3144
  const search = state.filters.search.trim().toLowerCase();
2434
3145
  const status = state.filters.status;
2435
3146
  const sort = state.filters.sort;
3147
+ const codexAccountId = config.codex && config.codex.accountId ? config.codex.accountId : "";
2436
3148
 
2437
3149
  const filtered = profiles.filter(function (profile) {
2438
3150
  const label = getProfileDisplayLabel(profile).toLowerCase();
@@ -2447,7 +3159,8 @@ function renderAdminPage() {
2447
3159
  }
2448
3160
 
2449
3161
  const health = getProfileHealth(profile);
2450
- if (status === "active" && !profile.isActive) {
3162
+ const isCodexActive = Boolean(codexAccountId && profile.accountId === codexAccountId);
3163
+ if (status === "active" && !profile.isActive && !isCodexActive) {
2451
3164
  return false;
2452
3165
  }
2453
3166
  if (status === "healthy" && health.key !== "healthy") {
@@ -2463,6 +3176,24 @@ function renderAdminPage() {
2463
3176
  });
2464
3177
 
2465
3178
  filtered.sort(function (a, b) {
3179
+ const aCodexActive = Boolean(codexAccountId && a.accountId === codexAccountId);
3180
+ const bCodexActive = Boolean(codexAccountId && b.accountId === codexAccountId);
3181
+ const activeDiff = Number(b.isActive || bCodexActive) - Number(a.isActive || aCodexActive);
3182
+ if (activeDiff !== 0) {
3183
+ return activeDiff;
3184
+ }
3185
+ const gatewayDiff = Number(b.isActive) - Number(a.isActive);
3186
+ if (gatewayDiff !== 0) {
3187
+ return gatewayDiff;
3188
+ }
3189
+ const codexDiff = Number(bCodexActive) - Number(aCodexActive);
3190
+ if (codexDiff !== 0) {
3191
+ return codexDiff;
3192
+ }
3193
+ const planDiff = getPlanRank(b) - getPlanRank(a);
3194
+ if (planDiff !== 0) {
3195
+ return planDiff;
3196
+ }
2466
3197
  if (sort === "latency-asc") {
2467
3198
  const aCapturedAt = getQuotaSnapshotTime(a) || 0;
2468
3199
  const bCapturedAt = getQuotaSnapshotTime(b) || 0;
@@ -2499,6 +3230,11 @@ function renderAdminPage() {
2499
3230
  delete state.selectedProfileIds[profileId];
2500
3231
  }
2501
3232
  });
3233
+ Object.keys(state.expandedProfileIds).forEach(function (profileId) {
3234
+ if (!availableIds[profileId]) {
3235
+ delete state.expandedProfileIds[profileId];
3236
+ }
3237
+ });
2502
3238
  }
2503
3239
 
2504
3240
  function updateSelectedProfileControls() {
@@ -2512,6 +3248,7 @@ function renderAdminPage() {
2512
3248
  syncSelectedProfiles(config);
2513
3249
  updateSelectedProfileControls();
2514
3250
  const profiles = getFilteredProfiles(config);
3251
+ const codexAccountId = config.codex && config.codex.accountId ? config.codex.accountId : "";
2515
3252
  const gridClass = profiles.length <= 0
2516
3253
  ? ""
2517
3254
  : profiles.length === 1
@@ -2530,22 +3267,29 @@ function renderAdminPage() {
2530
3267
 
2531
3268
  container.innerHTML = profiles.map(function (profile) {
2532
3269
  const selected = !!state.selectedProfileIds[profile.profileId];
2533
- const isSingleProfile = profiles.length === 1;
3270
+ const expanded = !!state.expandedProfileIds[profile.profileId];
2534
3271
  const health = getProfileHealth(profile);
2535
3272
  const planType = getPlanType(profile);
3273
+ const planKey = getPlanKey(profile);
2536
3274
  const imageCapability = getImageCapability(profile);
2537
3275
  const primary = getPrimaryUsage(profile);
2538
3276
  const secondary = getSecondaryUsage(profile);
2539
3277
  const primaryClass = health.barClass || "blue";
2540
3278
  const secondaryClass = secondary >= 85 ? "orange" : "blue";
3279
+ const isCodexActive = Boolean(codexAccountId && profile.accountId === codexAccountId);
3280
+ const usageCorner = getUsageCorner(profile, isCodexActive);
3281
+ const apiUsageClass = profile.isActive ? " is-active" : "";
3282
+ const codexUsageClass = isCodexActive ? " is-active" : "";
2541
3283
  const actionButton = profile.isActive
2542
- ? (isSingleProfile
2543
- ? '<span class="account-status">\u5F53\u524D\u4F7F\u7528\u4E2D</span>'
2544
- : '<button class="btn-secondary" type="button" disabled>\u5F53\u524D\u4F7F\u7528\u4E2D</button>')
2545
- : '<button class="btn-secondary" type="button" data-profile-action="activate" data-profile-id="' + escapeHtml(profile.profileId) + '">\u5207\u6362</button>';
3284
+ ? '<button class="btn-secondary is-current" type="button" disabled>\u7F51\u5173\u4F7F\u7528\u4E2D</button>'
3285
+ : '<button class="btn-secondary" type="button" data-profile-action="activate" data-profile-id="' + escapeHtml(profile.profileId) + '">\u5E94\u7528\u7F51\u5173</button>';
3286
+ const codexButton = isCodexActive
3287
+ ? '<button class="btn-secondary is-current codex" type="button" disabled>Codex \u4F7F\u7528\u4E2D</button>'
3288
+ : '<button class="btn-secondary" type="button" data-profile-action="apply-codex" data-profile-id="' + escapeHtml(profile.profileId) + '">\u5E94\u7528 Codex</button>';
2546
3289
 
2547
3290
  return ""
2548
- + '<article class="account-card" data-profile-card="' + escapeHtml(profile.profileId) + '">'
3291
+ + '<article class="account-card plan-' + escapeHtml(planKey) + '" data-profile-card="' + escapeHtml(profile.profileId) + '">'
3292
+ + (usageCorner ? '<span class="usage-corner ' + escapeHtml(usageCorner.className) + '"><span>' + escapeHtml(usageCorner.label) + "</span></span>" : "")
2549
3293
  + '<div class="account-head">'
2550
3294
  + '<div class="account-title">'
2551
3295
  + '<div class="account-name">'
@@ -2553,7 +3297,6 @@ function renderAdminPage() {
2553
3297
  + "<strong>" + escapeHtml(getProfileDisplayLabel(profile)) + "</strong>"
2554
3298
  + "</div>"
2555
3299
  + '<div class="badge-row">'
2556
- + (profile.isActive ? '<span class="badge blue">\u5F53\u524D\u4F7F\u7528</span>' : "")
2557
3300
  + '<span class="badge brand">' + escapeHtml(planType) + "</span>"
2558
3301
  + '<span class="badge ' + escapeHtml(health.badgeClass) + '">' + escapeHtml(health.label) + "</span>"
2559
3302
  + '<span class="badge ' + escapeHtml(imageCapability.badgeClass) + '">' + escapeHtml(imageCapability.label) + "</span>"
@@ -2571,18 +3314,45 @@ function renderAdminPage() {
2571
3314
  + '<div class="progress-track"><div class="progress-bar ' + escapeHtml(secondaryClass) + '" style="width:' + escapeHtml(String(secondary)) + '%"></div></div>'
2572
3315
  + "</div>"
2573
3316
  + "</div>"
2574
- + '<div class="meta-grid">'
2575
- + '<div class="meta-item"><label>\u5957\u9910</label><strong>' + escapeHtml(planType) + "</strong></div>"
2576
- + '<div class="meta-item"><label>\u751F\u56FE\u80FD\u529B</label><strong>' + escapeHtml(imageCapability.detail) + "</strong></div>"
2577
- + '<div class="meta-item"><label>\u91CD\u7F6E\u65F6\u95F4</label><strong>' + escapeHtml(describeReset(profile, "primary")) + "</strong></div>"
2578
- + '<div class="meta-item"><label>\u989D\u5EA6\u5FEB\u7167</label><strong>' + escapeHtml(describeQuotaSnapshot(profile)) + "</strong></div>"
2579
- + '<div class="meta-item"><label>\u989D\u5EA6\u9650\u5236</label><strong>' + escapeHtml(describeQuotaLimit(profile)) + "</strong></div>"
2580
- + '<div class="meta-item"><label>Account ID</label><code>' + escapeHtml(state.showEmails ? (profile.accountId || "\u672A\u63D0\u4F9B") : maskIdentifier(profile.accountId || "\u672A\u63D0\u4F9B")) + "</code></div>"
2581
- + '<div class="meta-item"><label>\u8FC7\u671F\u65F6\u95F4</label><span>' + escapeHtml(formatTime(profile.expiresAt)) + "</span></div>"
3317
+ + '<div class="usage-status-row">'
3318
+ + '<span class="usage-status' + apiUsageClass + '">'
3319
+ + '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"></circle><path d="M3 12h18"></path><path d="M12 3c2.5 2.7 3.8 5.7 3.8 9S14.5 18.3 12 21"></path><path d="M12 3c-2.5 2.7-3.8 5.7-3.8 9s1.3 6.3 3.8 9"></path></svg>'
3320
+ + "<span>API</span>"
3321
+ + '<span class="usage-dot' + (profile.isActive ? " active" : "") + '"></span>'
3322
+ + '<span class="usage-state-text">' + (profile.isActive ? "\u4F7F\u7528\u4E2D" : "\u672A\u4F7F\u7528") + "</span>"
3323
+ + "</span>"
3324
+ + '<span class="usage-status' + codexUsageClass + '">'
3325
+ + '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 17 10 11 4 5"></path><path d="M12 19h8"></path></svg>'
3326
+ + "<span>Codex</span>"
3327
+ + '<span class="usage-dot' + (isCodexActive ? " active" : "") + '"></span>'
3328
+ + '<span class="usage-state-text">' + (isCodexActive ? "\u4F7F\u7528\u4E2D" : "\u672A\u4F7F\u7528") + "</span>"
3329
+ + "</span>"
2582
3330
  + "</div>"
3331
+ + '<div class="compact-meta-row">'
3332
+ + '<div class="compact-reset-list">'
3333
+ + '<div class="compact-meta-item"><label>' + escapeHtml(getResetLabel(profile, "primary")) + '</label><strong>' + escapeHtml(describeCompactReset(profile, "primary")) + "</strong></div>"
3334
+ + '<div class="compact-meta-item"><label>' + escapeHtml(getResetLabel(profile, "secondary")) + '</label><strong>' + escapeHtml(describeCompactReset(profile, "secondary")) + "</strong></div>"
3335
+ + "</div>"
3336
+ + '<div class="compact-meta-actions">'
3337
+ + '<button class="details-toggle' + (expanded ? " is-expanded" : "") + '" type="button" data-profile-action="toggle-details" data-profile-id="' + escapeHtml(profile.profileId) + '">'
3338
+ + "<span>" + (expanded ? "\u6536\u8D77\u8BE6\u60C5" : "\u67E5\u770B\u8BE6\u60C5") + "</span>"
3339
+ + '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m6 9 6 6 6-6"></path></svg>'
3340
+ + "</button>"
3341
+ + "</div>"
3342
+ + "</div>"
3343
+ + (expanded
3344
+ ? '<div class="meta-grid">'
3345
+ + '<div class="meta-item"><label>\u5957\u9910</label><strong>' + escapeHtml(planType) + "</strong></div>"
3346
+ + '<div class="meta-item"><label>\u751F\u56FE\u80FD\u529B</label><strong>' + escapeHtml(imageCapability.detail) + "</strong></div>"
3347
+ + '<div class="meta-item"><label>\u989D\u5EA6\u5FEB\u7167</label><strong>' + escapeHtml(describeQuotaSnapshot(profile)) + "</strong></div>"
3348
+ + '<div class="meta-item"><label>\u989D\u5EA6\u9650\u5236</label><strong>' + escapeHtml(describeQuotaLimit(profile)) + "</strong></div>"
3349
+ + '<div class="meta-item"><label>Account ID</label><code>' + escapeHtml(state.showEmails ? (profile.accountId || "\u672A\u63D0\u4F9B") : maskIdentifier(profile.accountId || "\u672A\u63D0\u4F9B")) + "</code></div>"
3350
+ + '<div class="meta-item"><label>\u8FC7\u671F\u65F6\u95F4</label><span>' + escapeHtml(formatTime(profile.expiresAt)) + "</span></div>"
3351
+ + "</div>"
3352
+ : "")
2583
3353
  + '<div class="account-actions">'
2584
3354
  + actionButton
2585
- + '<button class="btn-secondary" type="button" data-profile-action="apply-codex" data-profile-id="' + escapeHtml(profile.profileId) + '">\u5E94\u7528\u5230 Codex</button>'
3355
+ + codexButton
2586
3356
  + '<button class="btn-secondary" type="button" data-profile-action="export" data-profile-id="' + escapeHtml(profile.profileId) + '">\u5BFC\u51FA</button>'
2587
3357
  + '<button class="btn-danger" type="button" data-profile-action="remove" data-profile-id="' + escapeHtml(profile.profileId) + '">\u5220\u9664</button>'
2588
3358
  + "</div>"
@@ -2747,6 +3517,7 @@ function renderAdminPage() {
2747
3517
  ["Provider", config.status.activeProvider || "openai-codex"],
2748
3518
  ["\u9ED8\u8BA4\u6A21\u578B", config.settings.defaultModel],
2749
3519
  ["\u4E0A\u6E38\u4EE3\u7406", config.settings.networkProxy && config.settings.networkProxy.enabled ? "\u5DF2\u542F\u7528" : "\u672A\u542F\u7528"],
3520
+ ["\u81EA\u52A8\u5207\u6362", config.settings.autoSwitch && config.settings.autoSwitch.enabled ? "\u5DF2\u542F\u7528" : "\u672A\u542F\u7528"],
2750
3521
  ["\u5F53\u524D\u7248\u672C", getVersionValue(config)],
2751
3522
  ["\u5F53\u524D\u5957\u9910", config.profile ? getPlanType(config.profile) : "\u672A\u767B\u5F55"],
2752
3523
  ["\u751F\u56FE\u80FD\u529B", getImageCapability(config.profile).detail],
@@ -2766,6 +3537,7 @@ function renderAdminPage() {
2766
3537
  ["\u5F53\u524D\u8D26\u53F7", getProfileDisplayLabel(config.profile)],
2767
3538
  ["\u9ED8\u8BA4\u6A21\u578B", config.settings.defaultModel],
2768
3539
  ["\u4E0A\u6E38\u4EE3\u7406", config.settings.networkProxy && config.settings.networkProxy.enabled ? config.settings.networkProxy.url : "\u672A\u542F\u7528"],
3540
+ ["\u81EA\u52A8\u5207\u6362", config.settings.autoSwitch && config.settings.autoSwitch.enabled ? "\u5DF2\u542F\u7528" : "\u672A\u542F\u7528"],
2769
3541
  ["\u7248\u672C\u72B6\u6001", getVersionDetail(config)],
2770
3542
  ["\u5F53\u524D\u5957\u9910", config.profile ? getPlanType(config.profile) : "\u672A\u767B\u5F55"],
2771
3543
  ["\u751F\u56FE\u80FD\u529B", getImageCapability(config.profile).detail],
@@ -2832,6 +3604,47 @@ function renderAdminPage() {
2832
3604
  proxyNoProxy.value = proxy.noProxy || "localhost,127.0.0.1,::1";
2833
3605
  }
2834
3606
 
3607
+ function renderAutoSwitchSettings(config) {
3608
+ const autoSwitch = config.settings.autoSwitch || {
3609
+ enabled: false,
3610
+ };
3611
+ autoSwitchEnabled.checked = !!autoSwitch.enabled;
3612
+ }
3613
+
3614
+ function isSettingsDrawerOpen() {
3615
+ return settingsDrawerBackdrop.classList.contains("is-open");
3616
+ }
3617
+
3618
+ function renderSettingsFields(config, options) {
3619
+ if (!config) {
3620
+ return;
3621
+ }
3622
+
3623
+ if (!(options && options.force) && state.settingsDirty && isSettingsDrawerOpen()) {
3624
+ return;
3625
+ }
3626
+
3627
+ renderModelOptions(config);
3628
+ renderModelCatalogStatus(config);
3629
+ renderProxySettings(config);
3630
+ renderAutoSwitchSettings(config);
3631
+ state.settingsDirty = false;
3632
+ }
3633
+
3634
+ function markSettingsDirty() {
3635
+ state.settingsDirty = true;
3636
+ }
3637
+
3638
+ function resetSettingsDraft() {
3639
+ state.settingsDirty = false;
3640
+ if (state.config) {
3641
+ renderSettingsFields(state.config, {
3642
+ force: true,
3643
+ });
3644
+ }
3645
+ settingsStatus.textContent = "";
3646
+ }
3647
+
2835
3648
  function syncHero(config) {
2836
3649
  const profileText = config.profile
2837
3650
  ? "\u5F53\u524D\u8D26\u53F7\u4E3A " + getProfileDisplayLabel(config.profile) + "\uFF0C\u5957\u9910 " + getPlanType(config.profile) + "\uFF0C\u53EF\u5728\u53F3\u4FA7\u5B8C\u6210\u6A21\u578B\u5207\u6362\u548C\u63A5\u53E3\u8C03\u8BD5\u3002"
@@ -2844,10 +3657,10 @@ function renderAdminPage() {
2844
3657
  const isImageEndpoint = endpointSelect.value === "/v1/images/generations" || endpointSelect.value === "/v1/images/edits";
2845
3658
  imageCapabilityHint.textContent = capability.detail;
2846
3659
  imageCapabilityHint.className = capability.supported && !isImageEndpoint ? "hint" : "hint warn";
2847
- runTestBtn.disabled = isImageEndpoint && !capability.supported;
3660
+ runTestBtn.disabled = isImageEndpoint && !config.profile;
2848
3661
  if (isImageEndpoint && !capability.supported) {
2849
3662
  testerMeta.textContent = capability.label;
2850
- } else if (testerMeta.textContent === capability.label || testerMeta.textContent === "\u751F\u56FE\u53D7\u9650") {
3663
+ } else if (testerMeta.textContent === capability.label || testerMeta.textContent === "\u751F\u56FE\u53D7\u9650" || testerMeta.textContent === "\u53EF\u5C1D\u8BD5\u751F\u56FE") {
2851
3664
  testerMeta.textContent = "\u51C6\u5907\u5C31\u7EEA";
2852
3665
  }
2853
3666
  }
@@ -2866,9 +3679,7 @@ function renderAdminPage() {
2866
3679
  syncHero(config);
2867
3680
  renderOverview(config);
2868
3681
  renderProfiles(config);
2869
- renderModelOptions(config);
2870
- renderModelCatalogStatus(config);
2871
- renderProxySettings(config);
3682
+ renderSettingsFields(config);
2872
3683
  renderUpdatePanel(config);
2873
3684
  renderEndpoints(config);
2874
3685
  renderServiceInfo(config);
@@ -2878,7 +3689,7 @@ function renderAdminPage() {
2878
3689
  authStatus.textContent = config.status.loggedIn
2879
3690
  ? (supportsImageGeneration(config.profile)
2880
3691
  ? "\u7F51\u5173\u5DF2\u53EF\u76F4\u63A5\u8F6C\u53D1\u8BF7\u6C42\uFF0C\u53EF\u4EE5\u5728\u4E0B\u65B9\u5207\u6362\u9ED8\u8BA4\u6A21\u578B\u5E76\u53D1\u9001\u6D4B\u8BD5\u8BF7\u6C42\u3002"
2881
- : "\u5F53\u524D\u8D26\u53F7\u53EF\u7528\u4E8E\u6587\u672C\u63A5\u53E3\uFF0C\u4F46\u4E0D\u652F\u6301\u56FE\u7247\u751F\u6210\u3002\u8BF7\u5207\u6362\u5230 Plus \u6216\u66F4\u9AD8\u5957\u9910\u8D26\u53F7\u3002")
3692
+ : "\u5F53\u524D\u8D26\u53F7\u672A\u5C31\u7EEA\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55\u540E\u518D\u6D4B\u8BD5\u63A5\u53E3\u3002")
2882
3693
  : "\u8BF7\u5148\u70B9\u51FB\u201C\u65B0\u589E\u8D26\u53F7\u201D\uFF0C\u5B8C\u6210 OAuth \u540E\u518D\u6D4B\u8BD5\u63A5\u53E3\u3002";
2883
3694
  if (!requestBody.value) {
2884
3695
  requestBody.value = buildExample(endpointSelect.value || "/v1/chat/completions");
@@ -2926,6 +3737,8 @@ function renderAdminPage() {
2926
3737
  const silent = !!(options && options.silent);
2927
3738
  const url = syncRuntime ? "/_gateway/admin/runtime-refresh" : "/_gateway/admin/config";
2928
3739
  const requestOptions = syncRuntime ? { method: "POST" } : undefined;
3740
+ const previousProfileId = state.config && state.config.profile ? state.config.profile.profileId : "";
3741
+ const previousStatus = authStatus.textContent;
2929
3742
 
2930
3743
  if (!silent) {
2931
3744
  testerMeta.textContent = syncRuntime ? "\u540C\u6B65\u989D\u5EA6\u4E0E\u7248\u672C\u72B6\u6001" : "\u5237\u65B0\u7BA1\u7406\u72B6\u6001";
@@ -2933,9 +3746,14 @@ function renderAdminPage() {
2933
3746
 
2934
3747
  const config = await fetchJson(url, requestOptions);
2935
3748
  renderConfig(config);
3749
+ const nextProfileId = config && config.profile ? config.profile.profileId : "";
2936
3750
 
2937
3751
  if (!silent) {
2938
3752
  testerMeta.textContent = "\u51C6\u5907\u5C31\u7EEA";
3753
+ } else if (previousProfileId && nextProfileId && previousProfileId !== nextProfileId) {
3754
+ authStatus.textContent = "\u68C0\u6D4B\u5230\u989D\u5EA6\u8017\u5C3D\uFF0C\u7F51\u5173\u5DF2\u81EA\u52A8\u5207\u6362\u5230: " + getProfileDisplayLabel(config.profile);
3755
+ } else {
3756
+ authStatus.textContent = previousStatus;
2939
3757
  }
2940
3758
 
2941
3759
  return config;
@@ -2956,6 +3774,20 @@ function renderAdminPage() {
2956
3774
  }, RUNTIME_AUTO_REFRESH_MS);
2957
3775
  }
2958
3776
 
3777
+ function scheduleActiveProfileRefresh() {
3778
+ window.setInterval(function () {
3779
+ if (document.hidden || !state.config || !state.config.settings || !state.config.settings.autoSwitch || !state.config.settings.autoSwitch.enabled) {
3780
+ return;
3781
+ }
3782
+
3783
+ refreshConfig({
3784
+ silent: true,
3785
+ }).catch(function (error) {
3786
+ console.warn("[admin] active profile refresh failed", error && error.message ? error.message : String(error));
3787
+ });
3788
+ }, ACTIVE_PROFILE_REFRESH_MS);
3789
+ }
3790
+
2959
3791
  async function syncQuotaAfterProfileChange(config, sourceLabel) {
2960
3792
  if (!config || !config.profile || config.profile.quota) {
2961
3793
  return config;
@@ -3018,6 +3850,10 @@ function renderAdminPage() {
3018
3850
  }
3019
3851
 
3020
3852
  async function runProfileAction(action, profileId, button) {
3853
+ if (!confirmQuotaSwitch(action, profileId)) {
3854
+ return;
3855
+ }
3856
+
3021
3857
  if (action === "export") {
3022
3858
  await exportProfile(profileId, button);
3023
3859
  return;
@@ -3075,7 +3911,7 @@ function renderAdminPage() {
3075
3911
  const config = result.config || await fetchJson("/_gateway/admin/config");
3076
3912
  renderConfig(config);
3077
3913
  const codex = result.codex || config.codex || {};
3078
- authStatus.textContent = "\u5DF2\u5E94\u7528\u5230 Codex\u3002\u65B0\u5F00\u7684 Codex \u4F1A\u8BDD\u5C06\u4F7F\u7528\u8BE5\u8D26\u53F7\u3002"
3914
+ authStatus.textContent = "\u5DF2\u5E94\u7528\u5230 Codex\u3002\u8BF7\u5173\u95ED Codex \u5E94\u7528\u5E76\u91CD\u65B0\u6253\u5F00\u540E\u751F\u6548\u3002"
3079
3915
  + (codex.backupPath ? " \u5DF2\u5907\u4EFD\u539F auth.json\u3002" : "");
3080
3916
  } catch (error) {
3081
3917
  authStatus.textContent = error.message;
@@ -3194,11 +4030,15 @@ function renderAdminPage() {
3194
4030
  });
3195
4031
  }
3196
4032
 
3197
- async function saveModel() {
3198
- const button = document.getElementById("saveModelBtn");
4033
+ async function saveSettings() {
3199
4034
  const select = document.getElementById("defaultModel");
3200
- setBusy(button, true);
3201
- authStatus.textContent = "\u6B63\u5728\u4FDD\u5B58\u9ED8\u8BA4\u6A21\u578B...";
4035
+ const savedProxy = state.config && state.config.settings && state.config.settings.networkProxy
4036
+ ? state.config.settings.networkProxy
4037
+ : { url: "", noProxy: "localhost,127.0.0.1,::1" };
4038
+ const nextProxyUrl = proxyUrl.value.trim() || (!proxyEnabled.checked ? savedProxy.url || "" : "");
4039
+ setBusy(saveSettingsBtn, true);
4040
+ settingsStatus.textContent = "\u6B63\u5728\u4FDD\u5B58\u8BBE\u7F6E...";
4041
+ authStatus.textContent = "\u6B63\u5728\u4FDD\u5B58\u8BBE\u7F6E...";
3202
4042
  try {
3203
4043
  const config = await fetchJson("/_gateway/admin/settings", {
3204
4044
  method: "PUT",
@@ -3207,24 +4047,36 @@ function renderAdminPage() {
3207
4047
  },
3208
4048
  body: formatJson({
3209
4049
  defaultModel: select.value,
4050
+ networkProxy: {
4051
+ enabled: proxyEnabled.checked,
4052
+ url: nextProxyUrl,
4053
+ noProxy: proxyNoProxy.value,
4054
+ },
4055
+ autoSwitch: {
4056
+ enabled: autoSwitchEnabled.checked,
4057
+ },
3210
4058
  }),
3211
4059
  });
4060
+ state.settingsDirty = false;
3212
4061
  renderConfig(config);
3213
- authStatus.textContent = "\u9ED8\u8BA4\u6A21\u578B\u5DF2\u66F4\u65B0\u3002";
4062
+ settingsStatus.textContent = "\u8BBE\u7F6E\u5DF2\u4FDD\u5B58\u3002";
4063
+ authStatus.textContent = "\u8BBE\u7F6E\u5DF2\u4FDD\u5B58\u3002";
3214
4064
  } catch (error) {
3215
- authStatus.textContent = error.message;
4065
+ const message = error && error.message ? error.message : String(error);
4066
+ settingsStatus.textContent = message;
4067
+ authStatus.textContent = message;
3216
4068
  } finally {
3217
- setBusy(button, false);
4069
+ setBusy(saveSettingsBtn, false);
3218
4070
  }
3219
4071
  }
3220
4072
 
3221
- async function saveProxy() {
3222
- const button = document.getElementById("saveProxyBtn");
3223
- setBusy(button, true);
3224
- authStatus.textContent = "\u6B63\u5728\u4FDD\u5B58\u4EE3\u7406\u914D\u7F6E...";
4073
+ async function testProxy() {
4074
+ setBusy(testProxyBtn, true);
4075
+ settingsStatus.textContent = "\u6B63\u5728\u6D4B\u8BD5\u4EE3\u7406\u8FDE\u63A5...";
4076
+ authStatus.textContent = "\u6B63\u5728\u6D4B\u8BD5\u4EE3\u7406\u8FDE\u63A5...";
3225
4077
  try {
3226
- const config = await fetchJson("/_gateway/admin/settings", {
3227
- method: "PUT",
4078
+ const result = await fetchJson("/_gateway/admin/settings/proxy-test", {
4079
+ method: "POST",
3228
4080
  headers: {
3229
4081
  "Content-Type": "application/json",
3230
4082
  },
@@ -3236,18 +4088,23 @@ function renderAdminPage() {
3236
4088
  },
3237
4089
  }),
3238
4090
  });
3239
- renderConfig(config);
3240
- authStatus.textContent = proxyEnabled.checked ? "\u4EE3\u7406\u914D\u7F6E\u5DF2\u542F\u7528\u3002" : "\u4EE3\u7406\u914D\u7F6E\u5DF2\u5173\u95ED\u3002";
4091
+ const message = "\u4EE3\u7406\u6D4B\u8BD5\u901A\u8FC7: HTTP " + String(result.status)
4092
+ + "\uFF0C\u8017\u65F6 " + String(result.elapsedMs) + " ms\u3002";
4093
+ settingsStatus.textContent = message;
4094
+ authStatus.textContent = message;
3241
4095
  } catch (error) {
3242
- authStatus.textContent = error.message;
4096
+ const message = "\u4EE3\u7406\u6D4B\u8BD5\u5931\u8D25: " + (error && error.message ? error.message : String(error));
4097
+ settingsStatus.textContent = message;
4098
+ authStatus.textContent = message;
3243
4099
  } finally {
3244
- setBusy(button, false);
4100
+ setBusy(testProxyBtn, false);
3245
4101
  }
3246
4102
  }
3247
4103
 
3248
4104
  async function refreshModels() {
3249
4105
  const button = document.getElementById("refreshModelsBtn");
3250
4106
  setBusy(button, true);
4107
+ settingsStatus.textContent = "\u6B63\u5728\u540C\u6B65 Codex \u6A21\u578B\u5217\u8868...";
3251
4108
  authStatus.textContent = "\u6B63\u5728\u540C\u6B65 Codex \u6A21\u578B\u5217\u8868...";
3252
4109
  try {
3253
4110
  await fetchJson("/_gateway/models/refresh", {
@@ -3255,9 +4112,12 @@ function renderAdminPage() {
3255
4112
  });
3256
4113
  const config = await fetchJson("/_gateway/admin/config");
3257
4114
  renderConfig(config);
4115
+ settingsStatus.textContent = "Codex \u6A21\u578B\u5217\u8868\u5DF2\u540C\u6B65\u3002";
3258
4116
  authStatus.textContent = "Codex \u6A21\u578B\u5217\u8868\u5DF2\u540C\u6B65\u3002";
3259
4117
  } catch (error) {
3260
- authStatus.textContent = error.message;
4118
+ const message = error && error.message ? error.message : String(error);
4119
+ settingsStatus.textContent = message;
4120
+ authStatus.textContent = message;
3261
4121
  } finally {
3262
4122
  setBusy(button, false);
3263
4123
  }
@@ -3278,6 +4138,7 @@ function renderAdminPage() {
3278
4138
  const meta = endpointMeta[endpoint];
3279
4139
  const button = document.getElementById("runTestBtn");
3280
4140
  const tracker = createTimingTracker();
4141
+ const initialProfileId = state.config && state.config.profile ? state.config.profile.profileId : "";
3281
4142
  setBusy(button, true);
3282
4143
  setTesterResultTab("response");
3283
4144
  testerMeta.textContent = "\u8BF7\u6C42\u4E2D: " + meta.method + " " + endpoint;
@@ -3359,6 +4220,13 @@ function renderAdminPage() {
3359
4220
  testerMeta.textContent = "\u8BF7\u6C42\u5931\u8D25";
3360
4221
  clearPreview();
3361
4222
  } finally {
4223
+ if (initialProfileId) {
4224
+ await refreshConfig({
4225
+ silent: true,
4226
+ }).catch(function (error) {
4227
+ console.warn("[admin] refresh after gateway request failed", error && error.message ? error.message : String(error));
4228
+ });
4229
+ }
3362
4230
  setBusy(button, false);
3363
4231
  }
3364
4232
  }
@@ -3392,7 +4260,27 @@ function renderAdminPage() {
3392
4260
  contactModal.setAttribute("aria-hidden", "true");
3393
4261
  }
3394
4262
 
4263
+ function openSettingsDrawer() {
4264
+ if (state.config && !state.settingsDirty) {
4265
+ renderSettingsFields(state.config, {
4266
+ force: true,
4267
+ });
4268
+ }
4269
+ settingsDrawerBackdrop.classList.add("is-open");
4270
+ settingsDrawerBackdrop.setAttribute("aria-hidden", "false");
4271
+ }
4272
+
4273
+ function closeSettingsDrawer() {
4274
+ resetSettingsDraft();
4275
+ settingsDrawerBackdrop.classList.remove("is-open");
4276
+ settingsDrawerBackdrop.setAttribute("aria-hidden", "true");
4277
+ }
4278
+
3395
4279
  document.getElementById("loginBtn").addEventListener("click", openAccountModal);
4280
+ document.getElementById("openSettingsBtn").addEventListener("click", openSettingsDrawer);
4281
+ document.querySelectorAll("[data-open-settings]").forEach(function (button) {
4282
+ button.addEventListener("click", openSettingsDrawer);
4283
+ });
3396
4284
  document.getElementById("refreshBtn").addEventListener("click", function () {
3397
4285
  authStatus.textContent = "\u6B63\u5728\u540C\u6B65\u989D\u5EA6\u4E0E\u7248\u672C\u72B6\u6001...";
3398
4286
  refreshConfig({
@@ -3412,9 +4300,14 @@ function renderAdminPage() {
3412
4300
  contactBtn.addEventListener("click", openContactModal);
3413
4301
  document.getElementById("closeContactBtn").addEventListener("click", closeContactModal);
3414
4302
  document.getElementById("closeImagePreviewBtn").addEventListener("click", closeImagePreviewModal);
4303
+ document.getElementById("closeSettingsDrawerBtn").addEventListener("click", closeSettingsDrawer);
3415
4304
  document.getElementById("refreshModelsBtn").addEventListener("click", refreshModels);
3416
- document.getElementById("saveModelBtn").addEventListener("click", saveModel);
3417
- document.getElementById("saveProxyBtn").addEventListener("click", saveProxy);
4305
+ testProxyBtn.addEventListener("click", testProxy);
4306
+ saveSettingsBtn.addEventListener("click", saveSettings);
4307
+ [document.getElementById("defaultModel"), proxyEnabled, proxyUrl, proxyNoProxy, autoSwitchEnabled].forEach(function (element) {
4308
+ element.addEventListener("input", markSettingsDirty);
4309
+ element.addEventListener("change", markSettingsDirty);
4310
+ });
3418
4311
  runTestBtn.addEventListener("click", runTest);
3419
4312
  document.querySelectorAll("[data-result-tab]").forEach(function (button) {
3420
4313
  button.addEventListener("click", function () {
@@ -3445,6 +4338,18 @@ function renderAdminPage() {
3445
4338
  return;
3446
4339
  }
3447
4340
 
4341
+ if (action === "toggle-details") {
4342
+ if (state.expandedProfileIds[profileId]) {
4343
+ delete state.expandedProfileIds[profileId];
4344
+ } else {
4345
+ state.expandedProfileIds[profileId] = true;
4346
+ }
4347
+ if (state.config) {
4348
+ renderProfiles(state.config);
4349
+ }
4350
+ return;
4351
+ }
4352
+
3448
4353
  runProfileAction(action, profileId, button);
3449
4354
  });
3450
4355
 
@@ -3544,6 +4449,12 @@ function renderAdminPage() {
3544
4449
  }
3545
4450
  });
3546
4451
 
4452
+ settingsDrawerBackdrop.addEventListener("click", function (event) {
4453
+ if (event.target === settingsDrawerBackdrop) {
4454
+ closeSettingsDrawer();
4455
+ }
4456
+ });
4457
+
3547
4458
  imagePreviewModal.addEventListener("click", function (event) {
3548
4459
  if (event.target === imagePreviewModal) {
3549
4460
  closeImagePreviewModal();
@@ -3560,10 +4471,14 @@ function renderAdminPage() {
3560
4471
  if (event.key === "Escape" && accountModal.classList.contains("is-open")) {
3561
4472
  closeAccountModal();
3562
4473
  }
4474
+ if (event.key === "Escape" && settingsDrawerBackdrop.classList.contains("is-open")) {
4475
+ closeSettingsDrawer();
4476
+ }
3563
4477
  });
3564
4478
 
3565
4479
  setTesterResultTab(state.testerResultTab);
3566
4480
  scheduleRuntimeRefresh();
4481
+ scheduleActiveProfileRefresh();
3567
4482
  refreshConfig().catch(function (error) {
3568
4483
  authStatus.textContent = error && error.message ? error.message : String(error);
3569
4484
  testerMeta.textContent = "\u52A0\u8F7D\u5931\u8D25";