ai-zero-token 1.0.8 → 1.0.10

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 {
@@ -807,9 +1100,148 @@ function renderAdminPage() {
807
1100
  background: var(--red-soft);
808
1101
  }
809
1102
 
810
- .account-metrics {
811
- display: grid;
812
- gap: 10px;
1103
+ .account-metrics {
1104
+ display: grid;
1105
+ gap: 10px;
1106
+ }
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);
813
1245
  }
814
1246
 
815
1247
  .quota-row {
@@ -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>
@@ -1427,8 +1898,9 @@ function renderAdminPage() {
1427
1898
  <option value="all">\u5168\u90E8\u72B6\u6001</option>
1428
1899
  <option value="healthy">\u5065\u5EB7</option>
1429
1900
  <option value="warning">\u5373\u5C06\u8017\u5C3D</option>
1901
+ <option value="invalid">\u767B\u5F55\u5931\u6548</option>
1430
1902
  <option value="expired">\u5DF2\u8FC7\u671F</option>
1431
- <option value="active">\u5F53\u524D\u4F7F\u7528</option>
1903
+ <option value="active">\u4F7F\u7528\u4E2D</option>
1432
1904
  </select>
1433
1905
  <select class="control" id="profileSort">
1434
1906
  <option value="quota-desc">\u6309\u4E3B\u989D\u5EA6\u6392\u5E8F</option>
@@ -1508,32 +1980,6 @@ function renderAdminPage() {
1508
1980
  <select class="control" id="endpointSelect"></select>
1509
1981
  </div>
1510
1982
 
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
1983
  <div class="field">
1538
1984
  <label for="requestBody">\u8BF7\u6C42\u4F53 JSON</label>
1539
1985
  <textarea class="textarea" id="requestBody" spellcheck="false"></textarea>
@@ -1597,6 +2043,65 @@ function renderAdminPage() {
1597
2043
  </main>
1598
2044
  </div>
1599
2045
 
2046
+ <div class="drawer-backdrop" id="settingsDrawerBackdrop" aria-hidden="true">
2047
+ <aside class="settings-drawer" role="dialog" aria-modal="true" aria-labelledby="settingsDrawerTitle">
2048
+ <div class="settings-drawer-head">
2049
+ <div>
2050
+ <h3 id="settingsDrawerTitle">\u7CFB\u7EDF\u8BBE\u7F6E</h3>
2051
+ <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>
2052
+ </div>
2053
+ <button class="btn-secondary" id="closeSettingsDrawerBtn" type="button">\u5173\u95ED</button>
2054
+ </div>
2055
+ <div class="settings-drawer-body">
2056
+ <section class="settings-section">
2057
+ <h4>\u9ED8\u8BA4\u6A21\u578B</h4>
2058
+ <div class="field">
2059
+ <label for="defaultModel">\u9ED8\u8BA4\u6A21\u578B</label>
2060
+ <select class="control" id="defaultModel"></select>
2061
+ <p class="hint">\u5F71\u54CD\u672A\u663E\u5F0F\u4F20 <code>model</code> \u7684\u8BF7\u6C42\u3002</p>
2062
+ <p class="hint" id="modelCatalogHint"></p>
2063
+ <div class="actions">
2064
+ <button class="btn-secondary" id="refreshModelsBtn" type="button">\u540C\u6B65 Codex \u6A21\u578B</button>
2065
+ </div>
2066
+ </div>
2067
+ </section>
2068
+
2069
+ <section class="settings-section">
2070
+ <h4>\u4E0A\u6E38\u4EE3\u7406</h4>
2071
+ <div class="field">
2072
+ <label class="checkbox-row" for="proxyEnabled">
2073
+ <input id="proxyEnabled" type="checkbox" />
2074
+ \u542F\u7528\u4E0A\u6E38\u4EE3\u7406
2075
+ </label>
2076
+ <label for="proxyUrl">\u4EE3\u7406\u5730\u5740</label>
2077
+ <input class="input" id="proxyUrl" type="text" placeholder="\u586B\u5199\u4F60\u7684\u4EE3\u7406\u5730\u5740" />
2078
+ <label for="proxyNoProxy">\u76F4\u8FDE\u5730\u5740</label>
2079
+ <input class="input" id="proxyNoProxy" type="text" placeholder="localhost,127.0.0.1,::1" />
2080
+ <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>
2081
+ <div class="actions">
2082
+ <button class="btn-secondary" id="testProxyBtn" type="button">\u6D4B\u8BD5\u4EE3\u7406</button>
2083
+ </div>
2084
+ </div>
2085
+ </section>
2086
+
2087
+ <section class="settings-section">
2088
+ <h4>\u8D26\u53F7\u5207\u6362</h4>
2089
+ <div class="field">
2090
+ <label class="checkbox-row" for="autoSwitchEnabled">
2091
+ <input id="autoSwitchEnabled" type="checkbox" />
2092
+ \u989D\u5EA6\u8017\u5C3D\u81EA\u52A8\u5207\u6362
2093
+ </label>
2094
+ <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>
2095
+ </div>
2096
+ </section>
2097
+ </div>
2098
+ <div class="settings-drawer-footer">
2099
+ <p class="status-inline" id="settingsStatus"></p>
2100
+ <button class="btn-primary" id="saveSettingsBtn" type="button">\u4FDD\u5B58\u8BBE\u7F6E</button>
2101
+ </div>
2102
+ </aside>
2103
+ </div>
2104
+
1600
2105
  <div class="modal-backdrop" id="imagePreviewModal" aria-hidden="true">
1601
2106
  <section class="modal-card preview-modal-card" role="dialog" aria-modal="true" aria-labelledby="imagePreviewTitle">
1602
2107
  <div class="modal-head">
@@ -1686,7 +2191,8 @@ function renderAdminPage() {
1686
2191
  </div>
1687
2192
 
1688
2193
  <script>
1689
- const RUNTIME_AUTO_REFRESH_MS = 10 * 60 * 1000;
2194
+ const RUNTIME_AUTO_REFRESH_MS = 5 * 60 * 1000;
2195
+ const ACTIVE_PROFILE_REFRESH_MS = 15 * 1000;
1690
2196
 
1691
2197
  const state = {
1692
2198
  config: null,
@@ -1698,7 +2204,9 @@ function renderAdminPage() {
1698
2204
  sort: "quota-desc",
1699
2205
  },
1700
2206
  selectedProfileIds: {},
2207
+ expandedProfileIds: {},
1701
2208
  testerResultTab: "response",
2209
+ settingsDirty: false,
1702
2210
  };
1703
2211
 
1704
2212
  const endpointMeta = {
@@ -1743,6 +2251,7 @@ function renderAdminPage() {
1743
2251
  const accountModal = document.getElementById("accountModal");
1744
2252
  const contactModal = document.getElementById("contactModal");
1745
2253
  const imagePreviewModal = document.getElementById("imagePreviewModal");
2254
+ const settingsDrawerBackdrop = document.getElementById("settingsDrawerBackdrop");
1746
2255
  const contactBtn = document.getElementById("contactBtn");
1747
2256
  const previewModalImage = document.getElementById("previewModalImage");
1748
2257
  const previewModalMeta = document.getElementById("previewModalMeta");
@@ -1759,6 +2268,10 @@ function renderAdminPage() {
1759
2268
  const proxyEnabled = document.getElementById("proxyEnabled");
1760
2269
  const proxyUrl = document.getElementById("proxyUrl");
1761
2270
  const proxyNoProxy = document.getElementById("proxyNoProxy");
2271
+ const testProxyBtn = document.getElementById("testProxyBtn");
2272
+ const autoSwitchEnabled = document.getElementById("autoSwitchEnabled");
2273
+ const settingsStatus = document.getElementById("settingsStatus");
2274
+ const saveSettingsBtn = document.getElementById("saveSettingsBtn");
1762
2275
 
1763
2276
  function setBusy(button, busy) {
1764
2277
  if (button) {
@@ -1790,6 +2303,13 @@ function renderAdminPage() {
1790
2303
  return date.toLocaleString("zh-CN", { hour12: false });
1791
2304
  }
1792
2305
 
2306
+ function timestampToMillis(value) {
2307
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
2308
+ return null;
2309
+ }
2310
+ return value < 1000000000000 ? value * 1000 : value;
2311
+ }
2312
+
1793
2313
  function formatShortTime(value) {
1794
2314
  if (!value) {
1795
2315
  return "--:--";
@@ -1850,6 +2370,9 @@ function renderAdminPage() {
1850
2370
  if (minutes === 60 * 24) {
1851
2371
  return "\u65E5\u989D\u5EA6";
1852
2372
  }
2373
+ if (minutes === 60 * 5) {
2374
+ return "5 \u5C0F\u65F6\u989D\u5EA6";
2375
+ }
1853
2376
  if (minutes === 60 * 24 * 7) {
1854
2377
  return "\u5468\u989D\u5EA6";
1855
2378
  }
@@ -1862,6 +2385,68 @@ function renderAdminPage() {
1862
2385
  : "unknown";
1863
2386
  }
1864
2387
 
2388
+ function getPlanRank(profile) {
2389
+ const plan = getPlanType(profile).toLowerCase();
2390
+ if (plan.indexOf("enterprise") !== -1 || plan.indexOf("business") !== -1) {
2391
+ return 60;
2392
+ }
2393
+ if (plan.indexOf("team") !== -1) {
2394
+ return 50;
2395
+ }
2396
+ if (plan.indexOf("pro") !== -1 || plan.indexOf("premium") !== -1) {
2397
+ return 40;
2398
+ }
2399
+ if (plan.indexOf("plus") !== -1) {
2400
+ return 30;
2401
+ }
2402
+ if (plan.indexOf("free") !== -1) {
2403
+ return 10;
2404
+ }
2405
+ return 0;
2406
+ }
2407
+
2408
+ function getPlanKey(profile) {
2409
+ const plan = getPlanType(profile).toLowerCase();
2410
+ if (plan.indexOf("enterprise") !== -1 || plan.indexOf("business") !== -1) {
2411
+ return "enterprise";
2412
+ }
2413
+ if (plan.indexOf("team") !== -1) {
2414
+ return "team";
2415
+ }
2416
+ if (plan.indexOf("pro") !== -1 || plan.indexOf("premium") !== -1) {
2417
+ return plan.indexOf("premium") !== -1 ? "premium" : "pro";
2418
+ }
2419
+ if (plan.indexOf("plus") !== -1) {
2420
+ return "plus";
2421
+ }
2422
+ if (plan.indexOf("free") !== -1) {
2423
+ return "free";
2424
+ }
2425
+ return "unknown";
2426
+ }
2427
+
2428
+ function getUsageCorner(profile, isCodexActive) {
2429
+ if (profile.isActive && isCodexActive) {
2430
+ return {
2431
+ className: "dual",
2432
+ label: "API + Codex",
2433
+ };
2434
+ }
2435
+ if (profile.isActive) {
2436
+ return {
2437
+ className: "api-only",
2438
+ label: "API",
2439
+ };
2440
+ }
2441
+ if (isCodexActive) {
2442
+ return {
2443
+ className: "codex-only",
2444
+ label: "Codex",
2445
+ };
2446
+ }
2447
+ return null;
2448
+ }
2449
+
1865
2450
  function getQuotaSnapshotTime(profile) {
1866
2451
  return profile && profile.quota && typeof profile.quota.capturedAt === "number"
1867
2452
  ? profile.quota.capturedAt
@@ -1951,6 +2536,15 @@ function renderAdminPage() {
1951
2536
  };
1952
2537
  }
1953
2538
 
2539
+ if (profile.authStatus && (profile.authStatus.state === "token_invalidated" || profile.authStatus.state === "auth_error")) {
2540
+ return {
2541
+ supported: false,
2542
+ label: "\u8BA4\u8BC1\u5931\u6548",
2543
+ detail: "\u8D26\u53F7\u8BA4\u8BC1\u5DF2\u5931\u6548\uFF0C\u8BF7\u91CD\u65B0\u767B\u5F55\u540E\u518D\u4F7F\u7528\u56FE\u7247\u751F\u6210\u3002",
2544
+ badgeClass: "red",
2545
+ };
2546
+ }
2547
+
1954
2548
  const planType = getPlanType(profile);
1955
2549
  if (planType === "free") {
1956
2550
  return {
@@ -1969,6 +2563,17 @@ function renderAdminPage() {
1969
2563
  };
1970
2564
  }
1971
2565
 
2566
+ function describeAuthStatus(profile) {
2567
+ const authStatus = profile && profile.authStatus ? profile.authStatus : null;
2568
+ if (!authStatus || authStatus.state === "ok") {
2569
+ return authStatus && authStatus.checkedAt ? "\u6B63\u5E38 \xB7 " + formatTime(authStatus.checkedAt) : "\u6B63\u5E38";
2570
+ }
2571
+
2572
+ const prefix = authStatus.state === "token_invalidated" ? "\u767B\u5F55\u5931\u6548" : "\u8BA4\u8BC1\u5F02\u5E38";
2573
+ const detail = authStatus.code || authStatus.httpStatus ? " (" + (authStatus.code || authStatus.httpStatus) + ")" : "";
2574
+ return prefix + detail + " \xB7 " + formatTime(authStatus.checkedAt);
2575
+ }
2576
+
1972
2577
  function maskEmail(email) {
1973
2578
  if (typeof email !== "string" || email.indexOf("@") === -1) {
1974
2579
  return email || "";
@@ -2050,8 +2655,59 @@ function renderAdminPage() {
2050
2655
  return Math.max(0, Math.min(100, value));
2051
2656
  }
2052
2657
 
2658
+ function isProfileQuotaExhausted(profile) {
2659
+ if (!profile || !profile.quota) {
2660
+ return false;
2661
+ }
2662
+
2663
+ return getPrimaryUsage(profile) >= 100 || getSecondaryUsage(profile) >= 100;
2664
+ }
2665
+
2666
+ function findProfileById(profileId) {
2667
+ const profiles = state.config && Array.isArray(state.config.profiles) ? state.config.profiles : [];
2668
+ return profiles.find(function (profile) {
2669
+ return profile.profileId === profileId;
2670
+ }) || null;
2671
+ }
2672
+
2673
+ function confirmQuotaSwitch(action, profileId) {
2674
+ if (action !== "activate" && action !== "apply-codex") {
2675
+ return true;
2676
+ }
2677
+
2678
+ const profile = findProfileById(profileId);
2679
+ if (!isProfileQuotaExhausted(profile)) {
2680
+ return true;
2681
+ }
2682
+
2683
+ const target = action === "activate" ? "\u7F51\u5173" : "Codex";
2684
+ const label = getProfileDisplayLabel(profile);
2685
+ 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";
2686
+ const confirmed = window.confirm(message);
2687
+ if (!confirmed) {
2688
+ authStatus.textContent = "\u5DF2\u53D6\u6D88\u5E94\u7528\u5230 " + target + "\u3002";
2689
+ }
2690
+ return confirmed;
2691
+ }
2692
+
2053
2693
  function getProfileHealth(profile) {
2054
2694
  const now = Date.now();
2695
+ if (profile && profile.authStatus && profile.authStatus.state === "token_invalidated") {
2696
+ return {
2697
+ key: "invalid",
2698
+ label: "\u767B\u5F55\u5931\u6548",
2699
+ badgeClass: "red",
2700
+ barClass: "red",
2701
+ };
2702
+ }
2703
+ if (profile && profile.authStatus && profile.authStatus.state === "auth_error") {
2704
+ return {
2705
+ key: "invalid",
2706
+ label: "\u8BA4\u8BC1\u5F02\u5E38",
2707
+ badgeClass: "red",
2708
+ barClass: "red",
2709
+ };
2710
+ }
2055
2711
  if (profile && profile.expiresAt && profile.expiresAt <= now) {
2056
2712
  return {
2057
2713
  key: "expired",
@@ -2096,8 +2752,9 @@ function renderAdminPage() {
2096
2752
  const quota = profile.quota;
2097
2753
  const resetAt = slot === "primary" ? quota.primaryResetAt : quota.secondaryResetAt;
2098
2754
  const resetAfter = slot === "primary" ? quota.primaryResetAfterSeconds : quota.secondaryResetAfterSeconds;
2099
- if (typeof resetAt === "number" && resetAt > 0) {
2100
- return formatTime(resetAt * 1000);
2755
+ const resetAtMillis = timestampToMillis(resetAt);
2756
+ if (resetAtMillis) {
2757
+ return formatTime(resetAtMillis);
2101
2758
  }
2102
2759
  if (typeof resetAfter === "number" && resetAfter > 0) {
2103
2760
  return formatCompactDuration(resetAfter) + "\u540E";
@@ -2105,6 +2762,39 @@ function renderAdminPage() {
2105
2762
  return "\u672A\u77E5";
2106
2763
  }
2107
2764
 
2765
+ function formatCompactDateTime(value) {
2766
+ if (!value) {
2767
+ return "\u6682\u65E0\u6570\u636E";
2768
+ }
2769
+ const date = new Date(value);
2770
+ if (Number.isNaN(date.getTime())) {
2771
+ return "\u672A\u77E5";
2772
+ }
2773
+ const month = String(date.getMonth() + 1);
2774
+ const day = String(date.getDate());
2775
+ const time = date.toLocaleTimeString("zh-CN", { hour12: false, hour: "2-digit", minute: "2-digit" });
2776
+ return month + "/" + day + " " + time;
2777
+ }
2778
+
2779
+ function describeCompactReset(profile, slot) {
2780
+ if (!profile || !profile.quota) {
2781
+ return "\u6682\u65E0\u6570\u636E";
2782
+ }
2783
+
2784
+ const quota = profile.quota;
2785
+ const resetAt = slot === "primary" ? quota.primaryResetAt : quota.secondaryResetAt;
2786
+ const resetAfter = slot === "primary" ? quota.primaryResetAfterSeconds : quota.secondaryResetAfterSeconds;
2787
+ const resetAtMillis = timestampToMillis(resetAt);
2788
+ if (resetAtMillis) {
2789
+ return formatCompactDateTime(resetAtMillis);
2790
+ }
2791
+ if (typeof resetAfter === "number" && resetAfter > 0) {
2792
+ const capturedAt = timestampToMillis(quota.capturedAt);
2793
+ return capturedAt ? formatCompactDateTime(capturedAt + resetAfter * 1000) : formatCompactDuration(resetAfter) + "\u540E";
2794
+ }
2795
+ return "\u672A\u77E5";
2796
+ }
2797
+
2108
2798
  function getQuotaWindowLabel(profile, slot) {
2109
2799
  const quota = profile && profile.quota ? profile.quota : null;
2110
2800
  if (!quota) {
@@ -2114,6 +2804,17 @@ function renderAdminPage() {
2114
2804
  return formatWindowLabel(quota && quota[field]);
2115
2805
  }
2116
2806
 
2807
+ function getResetLabel(profile, slot) {
2808
+ const label = getQuotaWindowLabel(profile, slot);
2809
+ if (label === "5 \u5C0F\u65F6\u989D\u5EA6") {
2810
+ return "5\u5C0F\u65F6\u91CD\u7F6E";
2811
+ }
2812
+ if (label === "\u5468\u989D\u5EA6") {
2813
+ return "\u5468\u91CD\u7F6E";
2814
+ }
2815
+ return label.replace("\u989D\u5EA6", "") + "\u91CD\u7F6E";
2816
+ }
2817
+
2117
2818
  function formatQuotaUsage(percent, profile, slot) {
2118
2819
  if (!profile || !profile.quota) {
2119
2820
  return "\u7B49\u5F85\u5237\u65B0";
@@ -2362,50 +3063,58 @@ function renderAdminPage() {
2362
3063
  const codexProfile = codexAccountId && Array.isArray(config.profiles)
2363
3064
  ? config.profiles.find(function (profile) { return profile.accountId === codexAccountId; })
2364
3065
  : null;
3066
+ const gatewayLabel = config.profile ? getProfileDisplayLabel(config.profile) : "\u672A\u6FC0\u6D3B\u8D26\u53F7";
3067
+ const codexLabel = codexProfile
3068
+ ? getProfileDisplayLabel(codexProfile)
3069
+ : (codexAccountId ? maskIdentifier(codexAccountId) : "\u672A\u68C0\u6D4B\u5230");
2365
3070
 
2366
3071
  return [
2367
3072
  {
3073
+ icon: "users",
3074
+ iconClass: "blue",
2368
3075
  label: "\u8D26\u53F7\u603B\u6570",
2369
3076
  value: String(config.status.profileCount || 0),
2370
3077
  detail: "\u5DF2\u4FDD\u5B58\u5230\u672C\u5730\u8D26\u53F7\u6C60",
2371
3078
  },
2372
3079
  {
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,
3080
+ kind: "account-status",
3081
+ label: "\u5F53\u524D\u8D26\u53F7\u72B6\u6001",
3082
+ gatewayLabel: gatewayLabel,
3083
+ codexLabel: codexLabel,
2379
3084
  },
2380
3085
  {
3086
+ icon: "model",
3087
+ iconClass: "brand",
2381
3088
  label: "\u9ED8\u8BA4\u6A21\u578B",
2382
3089
  value: config.settings.defaultModel || "-",
2383
3090
  detail: "\u672A\u663E\u5F0F\u6307\u5B9A model \u65F6\u751F\u6548",
2384
3091
  compact: true,
2385
3092
  },
2386
3093
  {
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
- {
3094
+ icon: "version",
3095
+ iconClass: config.versionStatus && config.versionStatus.needsUpdate ? "orange" : "green",
2393
3096
  label: "\u5F53\u524D\u7248\u672C",
2394
3097
  value: getVersionValue(config),
2395
3098
  detail: getVersionDetail(config),
2396
3099
  compact: true,
2397
3100
  },
2398
3101
  {
3102
+ icon: "requests",
3103
+ iconClass: "blue",
2399
3104
  label: "\u4ECA\u65E5\u8BF7\u6C42\u6570",
2400
3105
  value: String(requests.length),
2401
3106
  detail: "\u57FA\u4E8E\u672C\u9875\u6700\u8FD1\u6D4B\u8BD5\u8BB0\u5F55",
2402
3107
  },
2403
3108
  {
3109
+ icon: "latency",
3110
+ iconClass: "orange",
2404
3111
  label: "\u5E73\u5747\u8017\u65F6",
2405
3112
  value: requests.length ? (avg / 1000).toFixed(2) + " s" : "--",
2406
3113
  detail: requests.length ? "\u7EDF\u8BA1\u6700\u8FD1 " + String(requests.length) + " \u6B21" : "\u7B49\u5F85\u8BF7\u6C42\u6837\u672C",
2407
3114
  },
2408
3115
  {
3116
+ icon: "service",
3117
+ iconClass: config.status.loggedIn ? "green" : "orange",
2409
3118
  label: "\u670D\u52A1\u72B6\u6001",
2410
3119
  value: config.status.loggedIn ? "\u8FD0\u884C\u4E2D" : "\u5F85\u767B\u5F55",
2411
3120
  detail: config.status.loggedIn ? "\u7F51\u5173\u53EF\u8F6C\u53D1\u8BF7\u6C42" : "\u8BF7\u5148\u5B8C\u6210 OAuth \u767B\u5F55",
@@ -2414,14 +3123,63 @@ function renderAdminPage() {
2414
3123
  ];
2415
3124
  }
2416
3125
 
3126
+ function getSummaryIcon(name) {
3127
+ if (name === "users") {
3128
+ 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>';
3129
+ }
3130
+ if (name === "model") {
3131
+ 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>';
3132
+ }
3133
+ if (name === "version") {
3134
+ return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M20 6 9 17l-5-5"></path></svg>';
3135
+ }
3136
+ if (name === "requests") {
3137
+ 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>';
3138
+ }
3139
+ if (name === "latency") {
3140
+ 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>';
3141
+ }
3142
+ if (name === "service") {
3143
+ 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>';
3144
+ }
3145
+ return '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"></circle></svg>';
3146
+ }
3147
+
2417
3148
  function renderOverview(config) {
2418
3149
  const container = document.getElementById("summaryGrid");
2419
3150
  const cards = getOverviewCards(config);
2420
3151
  container.innerHTML = cards.map(function (card) {
3152
+ if (card.kind === "account-status") {
3153
+ return ""
3154
+ + '<article class="summary-card account-status-summary">'
3155
+ + '<div class="summary-card-head">'
3156
+ + '<span class="summary-icon green">'
3157
+ + '<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>'
3158
+ + "</span>"
3159
+ + "<label>" + escapeHtml(card.label) + "</label>"
3160
+ + "</div>"
3161
+ + '<div class="account-status-list">'
3162
+ + '<div class="account-status-line gateway">'
3163
+ + '<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>'
3164
+ + "<span>\u7F51\u5173\uFF1A</span>"
3165
+ + "<strong>" + escapeHtml(card.gatewayLabel) + "</strong>"
3166
+ + "</div>"
3167
+ + '<div class="account-status-line codex">'
3168
+ + '<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>'
3169
+ + "<span>Codex\uFF1A</span>"
3170
+ + "<strong>" + escapeHtml(card.codexLabel) + "</strong>"
3171
+ + "</div>"
3172
+ + "</div>"
3173
+ + "</article>";
3174
+ }
2421
3175
  const valueClass = card.compact ? "summary-value-sm" : "";
3176
+ const iconClass = card.iconClass ? " " + card.iconClass : "";
2422
3177
  return ""
2423
3178
  + '<article class="summary-card">'
2424
- + "<label>" + escapeHtml(card.label) + "</label>"
3179
+ + '<div class="summary-card-head">'
3180
+ + '<span class="summary-icon' + iconClass + '">' + getSummaryIcon(card.icon) + "</span>"
3181
+ + "<label>" + escapeHtml(card.label) + "</label>"
3182
+ + "</div>"
2425
3183
  + '<strong class="' + valueClass + '">' + escapeHtml(card.value) + "</strong>"
2426
3184
  + "<span>" + escapeHtml(card.detail) + "</span>"
2427
3185
  + "</article>";
@@ -2433,6 +3191,7 @@ function renderAdminPage() {
2433
3191
  const search = state.filters.search.trim().toLowerCase();
2434
3192
  const status = state.filters.status;
2435
3193
  const sort = state.filters.sort;
3194
+ const codexAccountId = config.codex && config.codex.accountId ? config.codex.accountId : "";
2436
3195
 
2437
3196
  const filtered = profiles.filter(function (profile) {
2438
3197
  const label = getProfileDisplayLabel(profile).toLowerCase();
@@ -2447,7 +3206,8 @@ function renderAdminPage() {
2447
3206
  }
2448
3207
 
2449
3208
  const health = getProfileHealth(profile);
2450
- if (status === "active" && !profile.isActive) {
3209
+ const isCodexActive = Boolean(codexAccountId && profile.accountId === codexAccountId);
3210
+ if (status === "active" && !profile.isActive && !isCodexActive) {
2451
3211
  return false;
2452
3212
  }
2453
3213
  if (status === "healthy" && health.key !== "healthy") {
@@ -2456,6 +3216,9 @@ function renderAdminPage() {
2456
3216
  if (status === "warning" && health.key !== "warning") {
2457
3217
  return false;
2458
3218
  }
3219
+ if (status === "invalid" && health.key !== "invalid") {
3220
+ return false;
3221
+ }
2459
3222
  if (status === "expired" && health.key !== "expired") {
2460
3223
  return false;
2461
3224
  }
@@ -2463,6 +3226,24 @@ function renderAdminPage() {
2463
3226
  });
2464
3227
 
2465
3228
  filtered.sort(function (a, b) {
3229
+ const aCodexActive = Boolean(codexAccountId && a.accountId === codexAccountId);
3230
+ const bCodexActive = Boolean(codexAccountId && b.accountId === codexAccountId);
3231
+ const activeDiff = Number(b.isActive || bCodexActive) - Number(a.isActive || aCodexActive);
3232
+ if (activeDiff !== 0) {
3233
+ return activeDiff;
3234
+ }
3235
+ const gatewayDiff = Number(b.isActive) - Number(a.isActive);
3236
+ if (gatewayDiff !== 0) {
3237
+ return gatewayDiff;
3238
+ }
3239
+ const codexDiff = Number(bCodexActive) - Number(aCodexActive);
3240
+ if (codexDiff !== 0) {
3241
+ return codexDiff;
3242
+ }
3243
+ const planDiff = getPlanRank(b) - getPlanRank(a);
3244
+ if (planDiff !== 0) {
3245
+ return planDiff;
3246
+ }
2466
3247
  if (sort === "latency-asc") {
2467
3248
  const aCapturedAt = getQuotaSnapshotTime(a) || 0;
2468
3249
  const bCapturedAt = getQuotaSnapshotTime(b) || 0;
@@ -2499,6 +3280,11 @@ function renderAdminPage() {
2499
3280
  delete state.selectedProfileIds[profileId];
2500
3281
  }
2501
3282
  });
3283
+ Object.keys(state.expandedProfileIds).forEach(function (profileId) {
3284
+ if (!availableIds[profileId]) {
3285
+ delete state.expandedProfileIds[profileId];
3286
+ }
3287
+ });
2502
3288
  }
2503
3289
 
2504
3290
  function updateSelectedProfileControls() {
@@ -2512,6 +3298,7 @@ function renderAdminPage() {
2512
3298
  syncSelectedProfiles(config);
2513
3299
  updateSelectedProfileControls();
2514
3300
  const profiles = getFilteredProfiles(config);
3301
+ const codexAccountId = config.codex && config.codex.accountId ? config.codex.accountId : "";
2515
3302
  const gridClass = profiles.length <= 0
2516
3303
  ? ""
2517
3304
  : profiles.length === 1
@@ -2530,22 +3317,29 @@ function renderAdminPage() {
2530
3317
 
2531
3318
  container.innerHTML = profiles.map(function (profile) {
2532
3319
  const selected = !!state.selectedProfileIds[profile.profileId];
2533
- const isSingleProfile = profiles.length === 1;
3320
+ const expanded = !!state.expandedProfileIds[profile.profileId];
2534
3321
  const health = getProfileHealth(profile);
2535
3322
  const planType = getPlanType(profile);
3323
+ const planKey = getPlanKey(profile);
2536
3324
  const imageCapability = getImageCapability(profile);
2537
3325
  const primary = getPrimaryUsage(profile);
2538
3326
  const secondary = getSecondaryUsage(profile);
2539
3327
  const primaryClass = health.barClass || "blue";
2540
3328
  const secondaryClass = secondary >= 85 ? "orange" : "blue";
3329
+ const isCodexActive = Boolean(codexAccountId && profile.accountId === codexAccountId);
3330
+ const usageCorner = getUsageCorner(profile, isCodexActive);
3331
+ const apiUsageClass = profile.isActive ? " is-active" : "";
3332
+ const codexUsageClass = isCodexActive ? " is-active" : "";
2541
3333
  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>';
3334
+ ? '<button class="btn-secondary is-current" type="button" disabled>\u7F51\u5173\u4F7F\u7528\u4E2D</button>'
3335
+ : '<button class="btn-secondary" type="button" data-profile-action="activate" data-profile-id="' + escapeHtml(profile.profileId) + '">\u5E94\u7528\u7F51\u5173</button>';
3336
+ const codexButton = isCodexActive
3337
+ ? '<button class="btn-secondary is-current codex" type="button" disabled>Codex \u4F7F\u7528\u4E2D</button>'
3338
+ : '<button class="btn-secondary" type="button" data-profile-action="apply-codex" data-profile-id="' + escapeHtml(profile.profileId) + '">\u5E94\u7528 Codex</button>';
2546
3339
 
2547
3340
  return ""
2548
- + '<article class="account-card" data-profile-card="' + escapeHtml(profile.profileId) + '">'
3341
+ + '<article class="account-card plan-' + escapeHtml(planKey) + '" data-profile-card="' + escapeHtml(profile.profileId) + '">'
3342
+ + (usageCorner ? '<span class="usage-corner ' + escapeHtml(usageCorner.className) + '"><span>' + escapeHtml(usageCorner.label) + "</span></span>" : "")
2549
3343
  + '<div class="account-head">'
2550
3344
  + '<div class="account-title">'
2551
3345
  + '<div class="account-name">'
@@ -2553,7 +3347,6 @@ function renderAdminPage() {
2553
3347
  + "<strong>" + escapeHtml(getProfileDisplayLabel(profile)) + "</strong>"
2554
3348
  + "</div>"
2555
3349
  + '<div class="badge-row">'
2556
- + (profile.isActive ? '<span class="badge blue">\u5F53\u524D\u4F7F\u7528</span>' : "")
2557
3350
  + '<span class="badge brand">' + escapeHtml(planType) + "</span>"
2558
3351
  + '<span class="badge ' + escapeHtml(health.badgeClass) + '">' + escapeHtml(health.label) + "</span>"
2559
3352
  + '<span class="badge ' + escapeHtml(imageCapability.badgeClass) + '">' + escapeHtml(imageCapability.label) + "</span>"
@@ -2571,18 +3364,47 @@ function renderAdminPage() {
2571
3364
  + '<div class="progress-track"><div class="progress-bar ' + escapeHtml(secondaryClass) + '" style="width:' + escapeHtml(String(secondary)) + '%"></div></div>'
2572
3365
  + "</div>"
2573
3366
  + "</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>"
3367
+ + '<div class="usage-status-row">'
3368
+ + '<span class="usage-status' + apiUsageClass + '">'
3369
+ + '<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>'
3370
+ + "<span>API</span>"
3371
+ + '<span class="usage-dot' + (profile.isActive ? " active" : "") + '"></span>'
3372
+ + '<span class="usage-state-text">' + (profile.isActive ? "\u4F7F\u7528\u4E2D" : "\u672A\u4F7F\u7528") + "</span>"
3373
+ + "</span>"
3374
+ + '<span class="usage-status' + codexUsageClass + '">'
3375
+ + '<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>'
3376
+ + "<span>Codex</span>"
3377
+ + '<span class="usage-dot' + (isCodexActive ? " active" : "") + '"></span>'
3378
+ + '<span class="usage-state-text">' + (isCodexActive ? "\u4F7F\u7528\u4E2D" : "\u672A\u4F7F\u7528") + "</span>"
3379
+ + "</span>"
3380
+ + "</div>"
3381
+ + '<div class="compact-meta-row">'
3382
+ + '<div class="compact-reset-list">'
3383
+ + '<div class="compact-meta-item"><label>' + escapeHtml(getResetLabel(profile, "primary")) + '</label><strong>' + escapeHtml(describeCompactReset(profile, "primary")) + "</strong></div>"
3384
+ + '<div class="compact-meta-item"><label>' + escapeHtml(getResetLabel(profile, "secondary")) + '</label><strong>' + escapeHtml(describeCompactReset(profile, "secondary")) + "</strong></div>"
3385
+ + "</div>"
3386
+ + '<div class="compact-meta-actions">'
3387
+ + '<button class="details-toggle' + (expanded ? " is-expanded" : "") + '" type="button" data-profile-action="toggle-details" data-profile-id="' + escapeHtml(profile.profileId) + '">'
3388
+ + "<span>" + (expanded ? "\u6536\u8D77\u8BE6\u60C5" : "\u67E5\u770B\u8BE6\u60C5") + "</span>"
3389
+ + '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="m6 9 6 6 6-6"></path></svg>'
3390
+ + "</button>"
3391
+ + "</div>"
2582
3392
  + "</div>"
3393
+ + (expanded
3394
+ ? '<div class="meta-grid">'
3395
+ + '<div class="meta-item"><label>\u5957\u9910</label><strong>' + escapeHtml(planType) + "</strong></div>"
3396
+ + '<div class="meta-item"><label>\u751F\u56FE\u80FD\u529B</label><strong>' + escapeHtml(imageCapability.detail) + "</strong></div>"
3397
+ + '<div class="meta-item"><label>\u8BA4\u8BC1\u72B6\u6001</label><strong>' + escapeHtml(describeAuthStatus(profile)) + "</strong></div>"
3398
+ + '<div class="meta-item"><label>\u989D\u5EA6\u5FEB\u7167</label><strong>' + escapeHtml(describeQuotaSnapshot(profile)) + "</strong></div>"
3399
+ + '<div class="meta-item"><label>\u989D\u5EA6\u9650\u5236</label><strong>' + escapeHtml(describeQuotaLimit(profile)) + "</strong></div>"
3400
+ + '<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>"
3401
+ + '<div class="meta-item"><label>\u8FC7\u671F\u65F6\u95F4</label><span>' + escapeHtml(formatTime(profile.expiresAt)) + "</span></div>"
3402
+ + "</div>"
3403
+ : "")
2583
3404
  + '<div class="account-actions">'
2584
3405
  + actionButton
2585
- + '<button class="btn-secondary" type="button" data-profile-action="apply-codex" data-profile-id="' + escapeHtml(profile.profileId) + '">\u5E94\u7528\u5230 Codex</button>'
3406
+ + codexButton
3407
+ + '<button class="btn-secondary" type="button" data-profile-action="sync-quota" data-profile-id="' + escapeHtml(profile.profileId) + '">\u5237\u65B0\u989D\u5EA6</button>'
2586
3408
  + '<button class="btn-secondary" type="button" data-profile-action="export" data-profile-id="' + escapeHtml(profile.profileId) + '">\u5BFC\u51FA</button>'
2587
3409
  + '<button class="btn-danger" type="button" data-profile-action="remove" data-profile-id="' + escapeHtml(profile.profileId) + '">\u5220\u9664</button>'
2588
3410
  + "</div>"
@@ -2747,6 +3569,7 @@ function renderAdminPage() {
2747
3569
  ["Provider", config.status.activeProvider || "openai-codex"],
2748
3570
  ["\u9ED8\u8BA4\u6A21\u578B", config.settings.defaultModel],
2749
3571
  ["\u4E0A\u6E38\u4EE3\u7406", config.settings.networkProxy && config.settings.networkProxy.enabled ? "\u5DF2\u542F\u7528" : "\u672A\u542F\u7528"],
3572
+ ["\u81EA\u52A8\u5207\u6362", config.settings.autoSwitch && config.settings.autoSwitch.enabled ? "\u5DF2\u542F\u7528" : "\u672A\u542F\u7528"],
2750
3573
  ["\u5F53\u524D\u7248\u672C", getVersionValue(config)],
2751
3574
  ["\u5F53\u524D\u5957\u9910", config.profile ? getPlanType(config.profile) : "\u672A\u767B\u5F55"],
2752
3575
  ["\u751F\u56FE\u80FD\u529B", getImageCapability(config.profile).detail],
@@ -2766,6 +3589,7 @@ function renderAdminPage() {
2766
3589
  ["\u5F53\u524D\u8D26\u53F7", getProfileDisplayLabel(config.profile)],
2767
3590
  ["\u9ED8\u8BA4\u6A21\u578B", config.settings.defaultModel],
2768
3591
  ["\u4E0A\u6E38\u4EE3\u7406", config.settings.networkProxy && config.settings.networkProxy.enabled ? config.settings.networkProxy.url : "\u672A\u542F\u7528"],
3592
+ ["\u81EA\u52A8\u5207\u6362", config.settings.autoSwitch && config.settings.autoSwitch.enabled ? "\u5DF2\u542F\u7528" : "\u672A\u542F\u7528"],
2769
3593
  ["\u7248\u672C\u72B6\u6001", getVersionDetail(config)],
2770
3594
  ["\u5F53\u524D\u5957\u9910", config.profile ? getPlanType(config.profile) : "\u672A\u767B\u5F55"],
2771
3595
  ["\u751F\u56FE\u80FD\u529B", getImageCapability(config.profile).detail],
@@ -2832,6 +3656,47 @@ function renderAdminPage() {
2832
3656
  proxyNoProxy.value = proxy.noProxy || "localhost,127.0.0.1,::1";
2833
3657
  }
2834
3658
 
3659
+ function renderAutoSwitchSettings(config) {
3660
+ const autoSwitch = config.settings.autoSwitch || {
3661
+ enabled: false,
3662
+ };
3663
+ autoSwitchEnabled.checked = !!autoSwitch.enabled;
3664
+ }
3665
+
3666
+ function isSettingsDrawerOpen() {
3667
+ return settingsDrawerBackdrop.classList.contains("is-open");
3668
+ }
3669
+
3670
+ function renderSettingsFields(config, options) {
3671
+ if (!config) {
3672
+ return;
3673
+ }
3674
+
3675
+ if (!(options && options.force) && state.settingsDirty && isSettingsDrawerOpen()) {
3676
+ return;
3677
+ }
3678
+
3679
+ renderModelOptions(config);
3680
+ renderModelCatalogStatus(config);
3681
+ renderProxySettings(config);
3682
+ renderAutoSwitchSettings(config);
3683
+ state.settingsDirty = false;
3684
+ }
3685
+
3686
+ function markSettingsDirty() {
3687
+ state.settingsDirty = true;
3688
+ }
3689
+
3690
+ function resetSettingsDraft() {
3691
+ state.settingsDirty = false;
3692
+ if (state.config) {
3693
+ renderSettingsFields(state.config, {
3694
+ force: true,
3695
+ });
3696
+ }
3697
+ settingsStatus.textContent = "";
3698
+ }
3699
+
2835
3700
  function syncHero(config) {
2836
3701
  const profileText = config.profile
2837
3702
  ? "\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"
@@ -2866,9 +3731,7 @@ function renderAdminPage() {
2866
3731
  syncHero(config);
2867
3732
  renderOverview(config);
2868
3733
  renderProfiles(config);
2869
- renderModelOptions(config);
2870
- renderModelCatalogStatus(config);
2871
- renderProxySettings(config);
3734
+ renderSettingsFields(config);
2872
3735
  renderUpdatePanel(config);
2873
3736
  renderEndpoints(config);
2874
3737
  renderServiceInfo(config);
@@ -2926,6 +3789,8 @@ function renderAdminPage() {
2926
3789
  const silent = !!(options && options.silent);
2927
3790
  const url = syncRuntime ? "/_gateway/admin/runtime-refresh" : "/_gateway/admin/config";
2928
3791
  const requestOptions = syncRuntime ? { method: "POST" } : undefined;
3792
+ const previousProfileId = state.config && state.config.profile ? state.config.profile.profileId : "";
3793
+ const previousStatus = authStatus.textContent;
2929
3794
 
2930
3795
  if (!silent) {
2931
3796
  testerMeta.textContent = syncRuntime ? "\u540C\u6B65\u989D\u5EA6\u4E0E\u7248\u672C\u72B6\u6001" : "\u5237\u65B0\u7BA1\u7406\u72B6\u6001";
@@ -2933,9 +3798,14 @@ function renderAdminPage() {
2933
3798
 
2934
3799
  const config = await fetchJson(url, requestOptions);
2935
3800
  renderConfig(config);
3801
+ const nextProfileId = config && config.profile ? config.profile.profileId : "";
2936
3802
 
2937
3803
  if (!silent) {
2938
3804
  testerMeta.textContent = "\u51C6\u5907\u5C31\u7EEA";
3805
+ } else if (previousProfileId && nextProfileId && previousProfileId !== nextProfileId) {
3806
+ authStatus.textContent = "\u68C0\u6D4B\u5230\u989D\u5EA6\u8017\u5C3D\uFF0C\u7F51\u5173\u5DF2\u81EA\u52A8\u5207\u6362\u5230: " + getProfileDisplayLabel(config.profile);
3807
+ } else {
3808
+ authStatus.textContent = previousStatus;
2939
3809
  }
2940
3810
 
2941
3811
  return config;
@@ -2956,6 +3826,20 @@ function renderAdminPage() {
2956
3826
  }, RUNTIME_AUTO_REFRESH_MS);
2957
3827
  }
2958
3828
 
3829
+ function scheduleActiveProfileRefresh() {
3830
+ window.setInterval(function () {
3831
+ if (document.hidden || !state.config || !state.config.settings || !state.config.settings.autoSwitch || !state.config.settings.autoSwitch.enabled) {
3832
+ return;
3833
+ }
3834
+
3835
+ refreshConfig({
3836
+ silent: true,
3837
+ }).catch(function (error) {
3838
+ console.warn("[admin] active profile refresh failed", error && error.message ? error.message : String(error));
3839
+ });
3840
+ }, ACTIVE_PROFILE_REFRESH_MS);
3841
+ }
3842
+
2959
3843
  async function syncQuotaAfterProfileChange(config, sourceLabel) {
2960
3844
  if (!config || !config.profile || config.profile.quota) {
2961
3845
  return config;
@@ -3018,6 +3902,10 @@ function renderAdminPage() {
3018
3902
  }
3019
3903
 
3020
3904
  async function runProfileAction(action, profileId, button) {
3905
+ if (!confirmQuotaSwitch(action, profileId)) {
3906
+ return;
3907
+ }
3908
+
3021
3909
  if (action === "export") {
3022
3910
  await exportProfile(profileId, button);
3023
3911
  return;
@@ -3026,6 +3914,28 @@ function renderAdminPage() {
3026
3914
  await applyProfileToCodex(profileId, button);
3027
3915
  return;
3028
3916
  }
3917
+ if (action === "sync-quota") {
3918
+ setBusy(button, true);
3919
+ authStatus.textContent = "\u6B63\u5728\u5237\u65B0\u8D26\u53F7\u989D\u5EA6...";
3920
+ try {
3921
+ const config = await fetchJson("/_gateway/admin/profiles/sync-quota", {
3922
+ method: "POST",
3923
+ headers: {
3924
+ "Content-Type": "application/json",
3925
+ },
3926
+ body: formatJson({
3927
+ profileId: profileId,
3928
+ }),
3929
+ });
3930
+ renderConfig(config);
3931
+ authStatus.textContent = "\u989D\u5EA6\u4FE1\u606F\u5DF2\u540C\u6B65\u3002";
3932
+ } catch (error) {
3933
+ authStatus.textContent = error.message;
3934
+ } finally {
3935
+ setBusy(button, false);
3936
+ }
3937
+ return;
3938
+ }
3029
3939
 
3030
3940
  setBusy(button, true);
3031
3941
  authStatus.textContent = action === "activate" ? "\u6B63\u5728\u5207\u6362\u5F53\u524D\u8D26\u53F7..." : "\u6B63\u5728\u5220\u9664\u8D26\u53F7...";
@@ -3075,7 +3985,7 @@ function renderAdminPage() {
3075
3985
  const config = result.config || await fetchJson("/_gateway/admin/config");
3076
3986
  renderConfig(config);
3077
3987
  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"
3988
+ authStatus.textContent = "\u5DF2\u5E94\u7528\u5230 Codex\u3002\u8BF7\u5173\u95ED Codex \u5E94\u7528\u5E76\u91CD\u65B0\u6253\u5F00\u540E\u751F\u6548\u3002"
3079
3989
  + (codex.backupPath ? " \u5DF2\u5907\u4EFD\u539F auth.json\u3002" : "");
3080
3990
  } catch (error) {
3081
3991
  authStatus.textContent = error.message;
@@ -3194,11 +4104,15 @@ function renderAdminPage() {
3194
4104
  });
3195
4105
  }
3196
4106
 
3197
- async function saveModel() {
3198
- const button = document.getElementById("saveModelBtn");
4107
+ async function saveSettings() {
3199
4108
  const select = document.getElementById("defaultModel");
3200
- setBusy(button, true);
3201
- authStatus.textContent = "\u6B63\u5728\u4FDD\u5B58\u9ED8\u8BA4\u6A21\u578B...";
4109
+ const savedProxy = state.config && state.config.settings && state.config.settings.networkProxy
4110
+ ? state.config.settings.networkProxy
4111
+ : { url: "", noProxy: "localhost,127.0.0.1,::1" };
4112
+ const nextProxyUrl = proxyUrl.value.trim() || (!proxyEnabled.checked ? savedProxy.url || "" : "");
4113
+ setBusy(saveSettingsBtn, true);
4114
+ settingsStatus.textContent = "\u6B63\u5728\u4FDD\u5B58\u8BBE\u7F6E...";
4115
+ authStatus.textContent = "\u6B63\u5728\u4FDD\u5B58\u8BBE\u7F6E...";
3202
4116
  try {
3203
4117
  const config = await fetchJson("/_gateway/admin/settings", {
3204
4118
  method: "PUT",
@@ -3207,24 +4121,36 @@ function renderAdminPage() {
3207
4121
  },
3208
4122
  body: formatJson({
3209
4123
  defaultModel: select.value,
4124
+ networkProxy: {
4125
+ enabled: proxyEnabled.checked,
4126
+ url: nextProxyUrl,
4127
+ noProxy: proxyNoProxy.value,
4128
+ },
4129
+ autoSwitch: {
4130
+ enabled: autoSwitchEnabled.checked,
4131
+ },
3210
4132
  }),
3211
4133
  });
4134
+ state.settingsDirty = false;
3212
4135
  renderConfig(config);
3213
- authStatus.textContent = "\u9ED8\u8BA4\u6A21\u578B\u5DF2\u66F4\u65B0\u3002";
4136
+ settingsStatus.textContent = "\u8BBE\u7F6E\u5DF2\u4FDD\u5B58\u3002";
4137
+ authStatus.textContent = "\u8BBE\u7F6E\u5DF2\u4FDD\u5B58\u3002";
3214
4138
  } catch (error) {
3215
- authStatus.textContent = error.message;
4139
+ const message = error && error.message ? error.message : String(error);
4140
+ settingsStatus.textContent = message;
4141
+ authStatus.textContent = message;
3216
4142
  } finally {
3217
- setBusy(button, false);
4143
+ setBusy(saveSettingsBtn, false);
3218
4144
  }
3219
4145
  }
3220
4146
 
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...";
4147
+ async function testProxy() {
4148
+ setBusy(testProxyBtn, true);
4149
+ settingsStatus.textContent = "\u6B63\u5728\u6D4B\u8BD5\u4EE3\u7406\u8FDE\u63A5...";
4150
+ authStatus.textContent = "\u6B63\u5728\u6D4B\u8BD5\u4EE3\u7406\u8FDE\u63A5...";
3225
4151
  try {
3226
- const config = await fetchJson("/_gateway/admin/settings", {
3227
- method: "PUT",
4152
+ const result = await fetchJson("/_gateway/admin/settings/proxy-test", {
4153
+ method: "POST",
3228
4154
  headers: {
3229
4155
  "Content-Type": "application/json",
3230
4156
  },
@@ -3236,18 +4162,23 @@ function renderAdminPage() {
3236
4162
  },
3237
4163
  }),
3238
4164
  });
3239
- renderConfig(config);
3240
- authStatus.textContent = proxyEnabled.checked ? "\u4EE3\u7406\u914D\u7F6E\u5DF2\u542F\u7528\u3002" : "\u4EE3\u7406\u914D\u7F6E\u5DF2\u5173\u95ED\u3002";
4165
+ const message = "\u4EE3\u7406\u6D4B\u8BD5\u901A\u8FC7: HTTP " + String(result.status)
4166
+ + "\uFF0C\u8017\u65F6 " + String(result.elapsedMs) + " ms\u3002";
4167
+ settingsStatus.textContent = message;
4168
+ authStatus.textContent = message;
3241
4169
  } catch (error) {
3242
- authStatus.textContent = error.message;
4170
+ const message = "\u4EE3\u7406\u6D4B\u8BD5\u5931\u8D25: " + (error && error.message ? error.message : String(error));
4171
+ settingsStatus.textContent = message;
4172
+ authStatus.textContent = message;
3243
4173
  } finally {
3244
- setBusy(button, false);
4174
+ setBusy(testProxyBtn, false);
3245
4175
  }
3246
4176
  }
3247
4177
 
3248
4178
  async function refreshModels() {
3249
4179
  const button = document.getElementById("refreshModelsBtn");
3250
4180
  setBusy(button, true);
4181
+ settingsStatus.textContent = "\u6B63\u5728\u540C\u6B65 Codex \u6A21\u578B\u5217\u8868...";
3251
4182
  authStatus.textContent = "\u6B63\u5728\u540C\u6B65 Codex \u6A21\u578B\u5217\u8868...";
3252
4183
  try {
3253
4184
  await fetchJson("/_gateway/models/refresh", {
@@ -3255,9 +4186,12 @@ function renderAdminPage() {
3255
4186
  });
3256
4187
  const config = await fetchJson("/_gateway/admin/config");
3257
4188
  renderConfig(config);
4189
+ settingsStatus.textContent = "Codex \u6A21\u578B\u5217\u8868\u5DF2\u540C\u6B65\u3002";
3258
4190
  authStatus.textContent = "Codex \u6A21\u578B\u5217\u8868\u5DF2\u540C\u6B65\u3002";
3259
4191
  } catch (error) {
3260
- authStatus.textContent = error.message;
4192
+ const message = error && error.message ? error.message : String(error);
4193
+ settingsStatus.textContent = message;
4194
+ authStatus.textContent = message;
3261
4195
  } finally {
3262
4196
  setBusy(button, false);
3263
4197
  }
@@ -3278,6 +4212,7 @@ function renderAdminPage() {
3278
4212
  const meta = endpointMeta[endpoint];
3279
4213
  const button = document.getElementById("runTestBtn");
3280
4214
  const tracker = createTimingTracker();
4215
+ const initialProfileId = state.config && state.config.profile ? state.config.profile.profileId : "";
3281
4216
  setBusy(button, true);
3282
4217
  setTesterResultTab("response");
3283
4218
  testerMeta.textContent = "\u8BF7\u6C42\u4E2D: " + meta.method + " " + endpoint;
@@ -3359,6 +4294,13 @@ function renderAdminPage() {
3359
4294
  testerMeta.textContent = "\u8BF7\u6C42\u5931\u8D25";
3360
4295
  clearPreview();
3361
4296
  } finally {
4297
+ if (initialProfileId) {
4298
+ await refreshConfig({
4299
+ silent: true,
4300
+ }).catch(function (error) {
4301
+ console.warn("[admin] refresh after gateway request failed", error && error.message ? error.message : String(error));
4302
+ });
4303
+ }
3362
4304
  setBusy(button, false);
3363
4305
  }
3364
4306
  }
@@ -3392,12 +4334,37 @@ function renderAdminPage() {
3392
4334
  contactModal.setAttribute("aria-hidden", "true");
3393
4335
  }
3394
4336
 
4337
+ function openSettingsDrawer() {
4338
+ if (state.config && !state.settingsDirty) {
4339
+ renderSettingsFields(state.config, {
4340
+ force: true,
4341
+ });
4342
+ }
4343
+ settingsDrawerBackdrop.classList.add("is-open");
4344
+ settingsDrawerBackdrop.setAttribute("aria-hidden", "false");
4345
+ }
4346
+
4347
+ function closeSettingsDrawer() {
4348
+ resetSettingsDraft();
4349
+ settingsDrawerBackdrop.classList.remove("is-open");
4350
+ settingsDrawerBackdrop.setAttribute("aria-hidden", "true");
4351
+ }
4352
+
3395
4353
  document.getElementById("loginBtn").addEventListener("click", openAccountModal);
4354
+ document.getElementById("openSettingsBtn").addEventListener("click", openSettingsDrawer);
4355
+ document.querySelectorAll("[data-open-settings]").forEach(function (button) {
4356
+ button.addEventListener("click", openSettingsDrawer);
4357
+ });
3396
4358
  document.getElementById("refreshBtn").addEventListener("click", function () {
3397
4359
  authStatus.textContent = "\u6B63\u5728\u540C\u6B65\u989D\u5EA6\u4E0E\u7248\u672C\u72B6\u6001...";
3398
4360
  refreshConfig({
3399
4361
  syncRuntime: true,
3400
- }).then(function () {
4362
+ }).then(function (config) {
4363
+ if (config && config.quotaSync) {
4364
+ const sync = config.quotaSync;
4365
+ authStatus.textContent = "\u989D\u5EA6\u4E0E\u7248\u672C\u72B6\u6001\u5DF2\u5237\u65B0: " + String(sync.synced) + "/" + String(sync.total) + " \u4E2A\u8D26\u53F7\u6210\u529F" + (sync.failed ? "\uFF0C" + String(sync.failed) + " \u4E2A\u5931\u8D25" : "") + (sync.skipped ? "\uFF0C" + String(sync.skipped) + " \u4E2A\u767B\u5F55\u5931\u6548\u5DF2\u8DF3\u8FC7" : "") + "\u3002";
4366
+ return;
4367
+ }
3401
4368
  authStatus.textContent = "\u989D\u5EA6\u4E0E\u7248\u672C\u72B6\u6001\u5DF2\u5237\u65B0\u3002";
3402
4369
  }).catch(function (error) {
3403
4370
  authStatus.textContent = error && error.message ? error.message : String(error);
@@ -3412,9 +4379,14 @@ function renderAdminPage() {
3412
4379
  contactBtn.addEventListener("click", openContactModal);
3413
4380
  document.getElementById("closeContactBtn").addEventListener("click", closeContactModal);
3414
4381
  document.getElementById("closeImagePreviewBtn").addEventListener("click", closeImagePreviewModal);
4382
+ document.getElementById("closeSettingsDrawerBtn").addEventListener("click", closeSettingsDrawer);
3415
4383
  document.getElementById("refreshModelsBtn").addEventListener("click", refreshModels);
3416
- document.getElementById("saveModelBtn").addEventListener("click", saveModel);
3417
- document.getElementById("saveProxyBtn").addEventListener("click", saveProxy);
4384
+ testProxyBtn.addEventListener("click", testProxy);
4385
+ saveSettingsBtn.addEventListener("click", saveSettings);
4386
+ [document.getElementById("defaultModel"), proxyEnabled, proxyUrl, proxyNoProxy, autoSwitchEnabled].forEach(function (element) {
4387
+ element.addEventListener("input", markSettingsDirty);
4388
+ element.addEventListener("change", markSettingsDirty);
4389
+ });
3418
4390
  runTestBtn.addEventListener("click", runTest);
3419
4391
  document.querySelectorAll("[data-result-tab]").forEach(function (button) {
3420
4392
  button.addEventListener("click", function () {
@@ -3445,6 +4417,18 @@ function renderAdminPage() {
3445
4417
  return;
3446
4418
  }
3447
4419
 
4420
+ if (action === "toggle-details") {
4421
+ if (state.expandedProfileIds[profileId]) {
4422
+ delete state.expandedProfileIds[profileId];
4423
+ } else {
4424
+ state.expandedProfileIds[profileId] = true;
4425
+ }
4426
+ if (state.config) {
4427
+ renderProfiles(state.config);
4428
+ }
4429
+ return;
4430
+ }
4431
+
3448
4432
  runProfileAction(action, profileId, button);
3449
4433
  });
3450
4434
 
@@ -3544,6 +4528,12 @@ function renderAdminPage() {
3544
4528
  }
3545
4529
  });
3546
4530
 
4531
+ settingsDrawerBackdrop.addEventListener("click", function (event) {
4532
+ if (event.target === settingsDrawerBackdrop) {
4533
+ closeSettingsDrawer();
4534
+ }
4535
+ });
4536
+
3547
4537
  imagePreviewModal.addEventListener("click", function (event) {
3548
4538
  if (event.target === imagePreviewModal) {
3549
4539
  closeImagePreviewModal();
@@ -3560,10 +4550,14 @@ function renderAdminPage() {
3560
4550
  if (event.key === "Escape" && accountModal.classList.contains("is-open")) {
3561
4551
  closeAccountModal();
3562
4552
  }
4553
+ if (event.key === "Escape" && settingsDrawerBackdrop.classList.contains("is-open")) {
4554
+ closeSettingsDrawer();
4555
+ }
3563
4556
  });
3564
4557
 
3565
4558
  setTesterResultTab(state.testerResultTab);
3566
4559
  scheduleRuntimeRefresh();
4560
+ scheduleActiveProfileRefresh();
3567
4561
  refreshConfig().catch(function (error) {
3568
4562
  authStatus.textContent = error && error.message ? error.message : String(error);
3569
4563
  testerMeta.textContent = "\u52A0\u8F7D\u5931\u8D25";