ai-zero-token 1.0.2 → 1.0.4

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.
@@ -208,11 +208,24 @@ function renderAdminPage() {
208
208
  gap: 10px;
209
209
  }
210
210
 
211
+ .service-list.compact {
212
+ grid-template-columns: repeat(2, minmax(0, 1fr));
213
+ gap: 10px 12px;
214
+ }
215
+
211
216
  .service-row {
212
217
  display: grid;
213
218
  gap: 4px;
214
219
  }
215
220
 
221
+ .service-row.compact {
222
+ padding: 10px 12px;
223
+ border-radius: 14px;
224
+ border: 1px solid var(--line);
225
+ background: var(--panel-soft);
226
+ min-width: 0;
227
+ }
228
+
216
229
  .service-row label {
217
230
  color: var(--text-muted);
218
231
  font-size: 12px;
@@ -272,6 +285,54 @@ function renderAdminPage() {
272
285
  gap: 14px;
273
286
  }
274
287
 
288
+ .update-panel {
289
+ display: none;
290
+ grid-template-columns: minmax(0, 1fr) auto;
291
+ align-items: center;
292
+ gap: 18px;
293
+ padding: 18px;
294
+ border-radius: 18px;
295
+ border: 1px solid rgba(245, 158, 11, 0.32);
296
+ background:
297
+ linear-gradient(135deg, rgba(255, 247, 237, 0.98), rgba(255, 251, 235, 0.92)),
298
+ var(--panel);
299
+ box-shadow: 0 18px 42px rgba(180, 83, 9, 0.12);
300
+ }
301
+
302
+ .update-panel.is-visible {
303
+ display: grid;
304
+ }
305
+
306
+ .update-copy {
307
+ display: grid;
308
+ gap: 7px;
309
+ min-width: 0;
310
+ }
311
+
312
+ .update-copy strong {
313
+ color: #9a3412;
314
+ font-size: 16px;
315
+ line-height: 1.35;
316
+ }
317
+
318
+ .update-copy span {
319
+ color: #7c2d12;
320
+ font-size: 13px;
321
+ line-height: 1.55;
322
+ overflow-wrap: anywhere;
323
+ }
324
+
325
+ .update-command {
326
+ padding: 10px 12px;
327
+ border-radius: 12px;
328
+ border: 1px solid rgba(180, 83, 9, 0.22);
329
+ background: rgba(255, 255, 255, 0.78);
330
+ color: #7c2d12;
331
+ font-size: 12px;
332
+ line-height: 1.4;
333
+ white-space: nowrap;
334
+ }
335
+
275
336
  .summary-card {
276
337
  border-radius: 16px;
277
338
  padding: 18px;
@@ -309,7 +370,7 @@ function renderAdminPage() {
309
370
 
310
371
  .main-grid {
311
372
  display: grid;
312
- grid-template-columns: minmax(0, 1fr) minmax(380px, 430px);
373
+ grid-template-columns: minmax(0, 1fr) minmax(460px, 560px);
313
374
  gap: 20px;
314
375
  align-items: start;
315
376
  }
@@ -588,17 +649,33 @@ function renderAdminPage() {
588
649
 
589
650
  .account-grid {
590
651
  display: grid;
591
- grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
592
652
  grid-auto-rows: 1fr;
593
653
  gap: 16px;
654
+ justify-content: start;
655
+ }
656
+
657
+ .account-grid.profile-count-1 {
658
+ grid-template-columns: minmax(300px, 440px);
659
+ }
660
+
661
+ .account-grid.profile-count-2 {
662
+ grid-template-columns: repeat(2, minmax(280px, 360px));
663
+ }
664
+
665
+ .account-grid.profile-count-3 {
666
+ grid-template-columns: repeat(3, minmax(260px, 320px));
667
+ }
668
+
669
+ .account-grid.profile-count-many {
670
+ grid-template-columns: repeat(auto-fit, minmax(250px, 300px));
594
671
  }
595
672
 
596
673
  .account-card {
597
- border-radius: 18px;
598
- padding: 16px;
674
+ border-radius: 16px;
675
+ padding: 14px;
599
676
  display: grid;
600
677
  grid-template-rows: auto auto auto 1fr auto;
601
- gap: 14px;
678
+ gap: 12px;
602
679
  min-width: 0;
603
680
  height: 100%;
604
681
  }
@@ -612,34 +689,34 @@ function renderAdminPage() {
612
689
 
613
690
  .account-title {
614
691
  display: grid;
615
- gap: 8px;
692
+ gap: 6px;
616
693
  min-width: 0;
617
694
  }
618
695
 
619
696
  .account-name {
620
697
  display: flex;
621
698
  align-items: center;
622
- gap: 10px;
699
+ gap: 8px;
623
700
  min-width: 0;
624
701
  }
625
702
 
626
703
  .avatar {
627
- width: 28px;
628
- height: 28px;
704
+ width: 24px;
705
+ height: 24px;
629
706
  border-radius: 999px;
630
707
  background: var(--panel-soft);
631
708
  border: 1px solid var(--line);
632
709
  display: grid;
633
710
  place-items: center;
634
- font-size: 12px;
711
+ font-size: 11px;
635
712
  color: var(--brand);
636
713
  font-weight: 700;
637
714
  flex: 0 0 auto;
638
715
  }
639
716
 
640
717
  .account-name strong {
641
- font-size: 14px;
642
- line-height: 1.4;
718
+ font-size: 13px;
719
+ line-height: 1.35;
643
720
  min-width: 0;
644
721
  display: -webkit-box;
645
722
  -webkit-line-clamp: 2;
@@ -653,17 +730,17 @@ function renderAdminPage() {
653
730
  .badge-row {
654
731
  display: flex;
655
732
  flex-wrap: wrap;
656
- gap: 8px;
733
+ gap: 6px;
657
734
  }
658
735
 
659
736
  .badge {
660
737
  display: inline-flex;
661
738
  align-items: center;
662
739
  justify-content: center;
663
- min-height: 24px;
664
- padding: 0 10px;
740
+ min-height: 22px;
741
+ padding: 0 8px;
665
742
  border-radius: 999px;
666
- font-size: 12px;
743
+ font-size: 11px;
667
744
  font-weight: 600;
668
745
  white-space: nowrap;
669
746
  }
@@ -695,26 +772,36 @@ function renderAdminPage() {
695
772
 
696
773
  .account-metrics {
697
774
  display: grid;
698
- gap: 12px;
775
+ gap: 10px;
699
776
  }
700
777
 
701
778
  .quota-row {
702
779
  display: grid;
703
- gap: 8px;
780
+ gap: 6px;
704
781
  }
705
782
 
706
783
  .quota-line {
707
784
  display: flex;
708
785
  align-items: center;
709
786
  justify-content: space-between;
710
- gap: 12px;
787
+ gap: 10px;
711
788
  color: var(--text-soft);
789
+ font-size: 11px;
790
+ line-height: 1.45;
791
+ }
792
+
793
+ .quota-line span {
794
+ min-width: 0;
795
+ }
796
+
797
+ .quota-line strong {
798
+ flex-shrink: 0;
799
+ color: var(--text);
712
800
  font-size: 12px;
713
- line-height: 1.5;
714
801
  }
715
802
 
716
803
  .progress-track {
717
- height: 6px;
804
+ height: 5px;
718
805
  width: 100%;
719
806
  border-radius: 999px;
720
807
  background: #eef2f7;
@@ -746,26 +833,26 @@ function renderAdminPage() {
746
833
  .meta-grid {
747
834
  display: grid;
748
835
  grid-template-columns: repeat(2, minmax(0, 1fr));
749
- gap: 10px 14px;
836
+ gap: 8px 12px;
750
837
  }
751
838
 
752
839
  .meta-item {
753
840
  display: grid;
754
- gap: 4px;
841
+ gap: 3px;
755
842
  min-width: 0;
756
843
  }
757
844
 
758
845
  .meta-item label {
759
846
  color: var(--text-muted);
760
- font-size: 11px;
847
+ font-size: 10px;
761
848
  line-height: 1.4;
762
849
  }
763
850
 
764
851
  .meta-item strong,
765
852
  .meta-item span,
766
853
  .meta-item code {
767
- font-size: 12px;
768
- line-height: 1.5;
854
+ font-size: 11px;
855
+ line-height: 1.45;
769
856
  color: var(--text-soft);
770
857
  word-break: break-word;
771
858
  overflow-wrap: anywhere;
@@ -774,13 +861,30 @@ function renderAdminPage() {
774
861
  .account-actions {
775
862
  display: flex;
776
863
  flex-wrap: wrap;
777
- gap: 10px;
864
+ gap: 8px;
778
865
  margin-top: auto;
779
866
  }
780
867
 
781
868
  .account-actions .btn-secondary,
782
869
  .account-actions .btn-danger {
783
870
  flex: 1 1 120px;
871
+ min-height: 36px;
872
+ padding: 0 12px;
873
+ border-radius: 10px;
874
+ font-size: 12px;
875
+ }
876
+
877
+ .account-status {
878
+ display: inline-flex;
879
+ align-items: center;
880
+ min-height: 36px;
881
+ padding: 0 12px;
882
+ border-radius: 10px;
883
+ background: var(--panel-soft);
884
+ border: 1px solid var(--line);
885
+ color: var(--text-muted);
886
+ font-size: 12px;
887
+ font-weight: 600;
784
888
  }
785
889
 
786
890
  .empty-state {
@@ -889,7 +993,8 @@ function renderAdminPage() {
889
993
  .pre {
890
994
  margin: 0;
891
995
  padding: 14px;
892
- min-height: 184px;
996
+ min-height: 280px;
997
+ max-height: 460px;
893
998
  overflow: auto;
894
999
  background: #0f172a;
895
1000
  border-color: #0f172a;
@@ -908,9 +1013,44 @@ function renderAdminPage() {
908
1013
  line-height: 1.6;
909
1014
  }
910
1015
 
1016
+ .tester-result {
1017
+ display: grid;
1018
+ gap: 12px;
1019
+ }
1020
+
1021
+ .tester-result-tabs {
1022
+ display: flex;
1023
+ flex-wrap: wrap;
1024
+ gap: 8px;
1025
+ }
1026
+
1027
+ .tester-result-tabs .tab-btn {
1028
+ border: 1px solid var(--line);
1029
+ background: #fff;
1030
+ }
1031
+
1032
+ .tester-panel {
1033
+ display: none;
1034
+ gap: 12px;
1035
+ }
1036
+
1037
+ .tester-panel.is-active {
1038
+ display: grid;
1039
+ }
1040
+
1041
+ .preview-empty {
1042
+ border: 1px dashed var(--line-strong);
1043
+ border-radius: 14px;
1044
+ padding: 18px;
1045
+ background: var(--panel-soft);
1046
+ color: var(--text-muted);
1047
+ font-size: 13px;
1048
+ line-height: 1.7;
1049
+ }
1050
+
911
1051
  .preview-grid {
912
1052
  display: grid;
913
- grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
1053
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
914
1054
  gap: 12px;
915
1055
  }
916
1056
 
@@ -927,11 +1067,12 @@ function renderAdminPage() {
927
1067
  .preview-card img {
928
1068
  display: block;
929
1069
  width: 100%;
930
- aspect-ratio: 1 / 1;
931
- object-fit: cover;
1070
+ aspect-ratio: 4 / 3;
1071
+ object-fit: contain;
932
1072
  border-radius: 10px;
933
1073
  border: 1px solid var(--line);
934
1074
  background: var(--panel-soft);
1075
+ cursor: zoom-in;
935
1076
  }
936
1077
 
937
1078
  .preview-card figcaption {
@@ -961,6 +1102,48 @@ function renderAdminPage() {
961
1102
  font-weight: 600;
962
1103
  }
963
1104
 
1105
+ .preview-modal-card {
1106
+ width: min(1120px, calc(100vw - 32px));
1107
+ }
1108
+
1109
+ .preview-modal-body {
1110
+ padding: 18px 20px 20px;
1111
+ display: grid;
1112
+ gap: 16px;
1113
+ }
1114
+
1115
+ .preview-modal-stage {
1116
+ display: grid;
1117
+ place-items: center;
1118
+ min-height: 420px;
1119
+ max-height: calc(100vh - 240px);
1120
+ overflow: auto;
1121
+ border: 1px solid var(--line);
1122
+ border-radius: 20px;
1123
+ background:
1124
+ linear-gradient(135deg, rgba(99, 91, 255, 0.06), transparent 42%),
1125
+ var(--panel-soft);
1126
+ padding: 16px;
1127
+ }
1128
+
1129
+ .preview-modal-stage img {
1130
+ display: block;
1131
+ max-width: 100%;
1132
+ max-height: calc(100vh - 280px);
1133
+ width: auto;
1134
+ height: auto;
1135
+ border-radius: 14px;
1136
+ background: #fff;
1137
+ box-shadow: var(--shadow);
1138
+ }
1139
+
1140
+ .preview-modal-meta {
1141
+ color: var(--text-soft);
1142
+ font-size: 13px;
1143
+ line-height: 1.7;
1144
+ word-break: break-word;
1145
+ }
1146
+
964
1147
  .log-table-wrap {
965
1148
  border-radius: 20px;
966
1149
  overflow: hidden;
@@ -1061,6 +1244,11 @@ function renderAdminPage() {
1061
1244
 
1062
1245
  .summary-grid,
1063
1246
  .account-grid,
1247
+ .account-grid.profile-count-1,
1248
+ .account-grid.profile-count-2,
1249
+ .account-grid.profile-count-3,
1250
+ .account-grid.profile-count-many,
1251
+ .preview-grid,
1064
1252
  .meta-grid,
1065
1253
  .trend-labels {
1066
1254
  grid-template-columns: 1fr;
@@ -1150,13 +1338,21 @@ function renderAdminPage() {
1150
1338
  <div class="top-actions">
1151
1339
  <a class="btn-link" href="https://github.com/fchangjun/AI-Zero-Token" target="_blank" rel="noreferrer">GitHub \u4ED3\u5E93</a>
1152
1340
  <button class="btn-secondary" id="contactBtn" type="button">\u4EA4\u6D41\u53CD\u9988</button>
1153
- <button class="btn-secondary" id="toggleEmailBtn" type="button">\u67E5\u770B\u90AE\u7BB1</button>
1341
+ <button class="btn-secondary" id="toggleEmailBtn" type="button">\u8131\u654F\u6A21\u5F0F</button>
1154
1342
  <button class="btn-primary" id="loginBtn" type="button">+ \u65B0\u589E\u8D26\u53F7</button>
1155
1343
  <button class="btn-secondary" id="refreshBtn" type="button">\u5237\u65B0\u72B6\u6001</button>
1156
1344
  <button class="btn-danger" id="logoutBtn" type="button">\u6E05\u7A7A\u8D26\u53F7</button>
1157
1345
  </div>
1158
1346
  </header>
1159
1347
 
1348
+ <section class="update-panel" id="updatePanel" aria-live="polite">
1349
+ <div class="update-copy">
1350
+ <strong id="updatePanelTitle">\u53D1\u73B0\u65B0\u7248\u672C</strong>
1351
+ <span id="updatePanelDetail"></span>
1352
+ </div>
1353
+ <code class="update-command" id="updatePanelCommand">npm install -g ai-zero-token@latest</code>
1354
+ </section>
1355
+
1160
1356
  <section class="summary-grid" id="summaryGrid"></section>
1161
1357
 
1162
1358
  <section class="main-grid">
@@ -1183,7 +1379,7 @@ function renderAdminPage() {
1183
1379
  </select>
1184
1380
  <select class="control" id="profileSort">
1185
1381
  <option value="quota-desc">\u6309\u4E3B\u989D\u5EA6\u6392\u5E8F</option>
1186
- <option value="latency-asc">\u6309\u8017\u65F6\u6392\u5E8F</option>
1382
+ <option value="latency-asc">\u6309\u989D\u5EA6\u66F4\u65B0\u65F6\u95F4</option>
1187
1383
  <option value="expiry-asc">\u6309\u8FC7\u671F\u65F6\u95F4</option>
1188
1384
  <option value="name-asc">\u6309\u90AE\u7BB1\u6392\u5E8F</option>
1189
1385
  </select>
@@ -1262,7 +1458,9 @@ function renderAdminPage() {
1262
1458
  <label for="defaultModel">\u9ED8\u8BA4\u6A21\u578B</label>
1263
1459
  <select class="control" id="defaultModel"></select>
1264
1460
  <p class="hint">\u5207\u6362\u540E\u4F1A\u5F71\u54CD\u672A\u663E\u5F0F\u4F20 <code>model</code> \u7684\u8BF7\u6C42\u3002</p>
1461
+ <p class="hint" id="modelCatalogHint"></p>
1265
1462
  <div class="actions">
1463
+ <button class="btn-secondary" id="refreshModelsBtn" type="button">\u540C\u6B65 Codex \u6A21\u578B</button>
1266
1464
  <button class="btn-primary" id="saveModelBtn" type="button">\u4FDD\u5B58\u9ED8\u8BA4\u6A21\u578B</button>
1267
1465
  </div>
1268
1466
  </div>
@@ -1284,33 +1482,72 @@ function renderAdminPage() {
1284
1482
 
1285
1483
  <p class="status-inline" id="authStatus"></p>
1286
1484
 
1287
- <div class="field">
1288
- <label for="responseBody">\u54CD\u5E94\u7ED3\u679C</label>
1289
- <pre class="pre" id="responseBody">\u7B49\u5F85\u8BF7\u6C42\u2026</pre>
1290
- </div>
1485
+ <div class="tester-result">
1486
+ <div class="tester-result-tabs">
1487
+ <button class="tab-btn is-active" type="button" data-result-tab="response">\u54CD\u5E94 JSON</button>
1488
+ <button class="tab-btn" type="button" data-result-tab="timing">\u8017\u65F6\u65E5\u5FD7</button>
1489
+ <button class="tab-btn" type="button" data-result-tab="preview">\u56FE\u7247\u9884\u89C8</button>
1490
+ </div>
1291
1491
 
1292
- <div class="field">
1293
- <label for="timingBody">\u8017\u65F6\u65E5\u5FD7</label>
1294
- <pre class="pre" id="timingBody">\u7B49\u5F85\u8BF7\u6C42\u2026</pre>
1295
- </div>
1492
+ <div class="tester-panel is-active" data-result-panel="response">
1493
+ <div class="field">
1494
+ <label for="responseBody">\u54CD\u5E94\u7ED3\u679C</label>
1495
+ <pre class="pre" id="responseBody">\u7B49\u5F85\u8BF7\u6C42\u2026</pre>
1496
+ </div>
1497
+ </div>
1296
1498
 
1297
- <div class="preview-grid" id="responsePreview" hidden></div>
1499
+ <div class="tester-panel" data-result-panel="timing">
1500
+ <div class="field">
1501
+ <label for="timingBody">\u8017\u65F6\u65E5\u5FD7</label>
1502
+ <pre class="pre" id="timingBody">\u7B49\u5F85\u8BF7\u6C42\u2026</pre>
1503
+ </div>
1504
+ </div>
1505
+
1506
+ <div class="tester-panel" data-result-panel="preview">
1507
+ <div class="field">
1508
+ <label>\u56FE\u7247\u9884\u89C8</label>
1509
+ <div class="preview-empty" id="responsePreviewEmpty">\u56FE\u7247\u7ED3\u679C\u4F1A\u663E\u793A\u5728\u8FD9\u91CC\u3002\u70B9\u51FB\u7F29\u7565\u56FE\u53EF\u67E5\u770B\u5927\u56FE\u3002</div>
1510
+ <div class="preview-grid" id="responsePreview"></div>
1511
+ </div>
1512
+ </div>
1513
+ </div>
1298
1514
  </section>
1299
1515
 
1300
- <section class="card">
1516
+ <section class="card service-card">
1301
1517
  <div class="section-head" style="margin-bottom: 12px;">
1302
1518
  <div>
1303
- <h2>\u63A5\u53E3\u4FE1\u606F</h2>
1519
+ <h2>\u8FDE\u63A5\u4FE1\u606F</h2>
1304
1520
  <p>\u7BA1\u7406\u9875\u548C API Base \u53EF\u76F4\u63A5\u590D\u5236\u5230 SDK \u6216\u6D4B\u8BD5\u5DE5\u5177\u3002</p>
1305
1521
  </div>
1306
1522
  </div>
1307
- <div class="service-list" id="endpointInfo"></div>
1523
+ <div class="service-list compact" id="endpointInfo"></div>
1308
1524
  </section>
1309
1525
  </aside>
1310
1526
  </section>
1311
1527
  </main>
1312
1528
  </div>
1313
1529
 
1530
+ <div class="modal-backdrop" id="imagePreviewModal" aria-hidden="true">
1531
+ <section class="modal-card preview-modal-card" role="dialog" aria-modal="true" aria-labelledby="imagePreviewTitle">
1532
+ <div class="modal-head">
1533
+ <div>
1534
+ <h3 id="imagePreviewTitle">\u56FE\u7247\u9884\u89C8</h3>
1535
+ <p>\u67E5\u770B\u751F\u56FE\u7ED3\u679C\u7684\u66F4\u5927\u7248\u672C\uFF0C\u907F\u514D\u5728\u4FA7\u680F\u5185\u6EDA\u52A8\u67E5\u770B\u3002</p>
1536
+ </div>
1537
+ <div class="actions">
1538
+ <a class="btn-secondary" id="downloadPreviewBtn" href="#" download="generated-image.png">\u4E0B\u8F7D\u56FE\u7247</a>
1539
+ <button class="btn-secondary" id="closeImagePreviewBtn" type="button">\u5173\u95ED</button>
1540
+ </div>
1541
+ </div>
1542
+ <div class="preview-modal-body">
1543
+ <div class="preview-modal-stage">
1544
+ <img id="previewModalImage" src="" alt="\u751F\u6210\u56FE\u7247\u9884\u89C8" />
1545
+ </div>
1546
+ <div class="preview-modal-meta" id="previewModalMeta">\u7B49\u5F85\u56FE\u7247\u7ED3\u679C\u2026</div>
1547
+ </div>
1548
+ </section>
1549
+ </div>
1550
+
1314
1551
  <div class="modal-backdrop" id="contactModal" aria-hidden="true">
1315
1552
  <section class="modal-card" role="dialog" aria-modal="true" aria-labelledby="contactModalTitle">
1316
1553
  <div class="modal-head">
@@ -1344,6 +1581,8 @@ function renderAdminPage() {
1344
1581
  </div>
1345
1582
 
1346
1583
  <script>
1584
+ const RUNTIME_AUTO_REFRESH_MS = 10 * 60 * 1000;
1585
+
1347
1586
  const state = {
1348
1587
  config: null,
1349
1588
  recentRequests: [],
@@ -1353,6 +1592,7 @@ function renderAdminPage() {
1353
1592
  status: "all",
1354
1593
  sort: "quota-desc",
1355
1594
  },
1595
+ testerResultTab: "response",
1356
1596
  };
1357
1597
 
1358
1598
  const endpointMeta = {
@@ -1382,6 +1622,7 @@ function renderAdminPage() {
1382
1622
  const requestBody = document.getElementById("requestBody");
1383
1623
  const responseBody = document.getElementById("responseBody");
1384
1624
  const responsePreview = document.getElementById("responsePreview");
1625
+ const responsePreviewEmpty = document.getElementById("responsePreviewEmpty");
1385
1626
  const timingBody = document.getElementById("timingBody");
1386
1627
  const testerMeta = document.getElementById("testerMeta");
1387
1628
  const authStatus = document.getElementById("authStatus");
@@ -1389,7 +1630,11 @@ function renderAdminPage() {
1389
1630
  const runTestBtn = document.getElementById("runTestBtn");
1390
1631
  const toggleEmailBtn = document.getElementById("toggleEmailBtn");
1391
1632
  const contactModal = document.getElementById("contactModal");
1633
+ const imagePreviewModal = document.getElementById("imagePreviewModal");
1392
1634
  const contactBtn = document.getElementById("contactBtn");
1635
+ const previewModalImage = document.getElementById("previewModalImage");
1636
+ const previewModalMeta = document.getElementById("previewModalMeta");
1637
+ const downloadPreviewBtn = document.getElementById("downloadPreviewBtn");
1393
1638
  const profileSearch = document.getElementById("profileSearch");
1394
1639
  const profileStatusFilter = document.getElementById("profileStatusFilter");
1395
1640
  const profileSort = document.getElementById("profileSort");
@@ -1496,6 +1741,81 @@ function renderAdminPage() {
1496
1741
  : "unknown";
1497
1742
  }
1498
1743
 
1744
+ function getQuotaSnapshotTime(profile) {
1745
+ return profile && profile.quota && typeof profile.quota.capturedAt === "number"
1746
+ ? profile.quota.capturedAt
1747
+ : 0;
1748
+ }
1749
+
1750
+ function describeQuotaSnapshot(profile) {
1751
+ const capturedAt = getQuotaSnapshotTime(profile);
1752
+ return capturedAt > 0 ? formatTime(capturedAt) : "\u672A\u540C\u6B65";
1753
+ }
1754
+
1755
+ function describeQuotaLimit(profile) {
1756
+ const quota = profile && profile.quota ? profile.quota : null;
1757
+ if (!quota) {
1758
+ return "\u7B49\u5F85\u5237\u65B0\u989D\u5EA6";
1759
+ }
1760
+
1761
+ const parts = [];
1762
+ if (typeof quota.activeLimit === "string" && quota.activeLimit) {
1763
+ parts.push(quota.activeLimit);
1764
+ }
1765
+ if (typeof quota.creditsBalance === "string" && quota.creditsBalance) {
1766
+ parts.push("\u4F59\u989D " + quota.creditsBalance);
1767
+ }
1768
+ if (typeof quota.primaryOverSecondaryLimitPercent === "number") {
1769
+ parts.push("\u5171\u4EAB\u4E0A\u9650 " + Math.round(quota.primaryOverSecondaryLimitPercent) + "%");
1770
+ }
1771
+
1772
+ return parts.length > 0 ? parts.join(" \xB7 ") : "\u6765\u81EA Codex \u54CD\u5E94\u5934";
1773
+ }
1774
+
1775
+ function getVersionValue(config) {
1776
+ if (!config || !config.versionStatus) {
1777
+ return "--";
1778
+ }
1779
+
1780
+ return config.versionStatus.currentVersion || "--";
1781
+ }
1782
+
1783
+ function getVersionDetail(config) {
1784
+ if (!config || !config.versionStatus) {
1785
+ return "\u7248\u672C\u72B6\u6001\u672A\u77E5";
1786
+ }
1787
+
1788
+ const versionStatus = config.versionStatus;
1789
+ if (versionStatus.status === "update-available" && versionStatus.latestVersion) {
1790
+ return "\u53EF\u66F4\u65B0\u5230 " + versionStatus.latestVersion;
1791
+ }
1792
+ if (versionStatus.status === "ok" && versionStatus.latestVersion) {
1793
+ return "\u5DF2\u68C0\u67E5\uFF0C\u5F53\u524D\u5DF2\u662F\u6700\u65B0\u7248\u672C";
1794
+ }
1795
+ return versionStatus.error
1796
+ ? "\u7248\u672C\u68C0\u67E5\u5931\u8D25: " + versionStatus.error
1797
+ : "\u6682\u672A\u62FF\u5230\u8FDC\u7AEF\u7248\u672C\u4FE1\u606F";
1798
+ }
1799
+
1800
+ function renderUpdatePanel(config) {
1801
+ const panel = document.getElementById("updatePanel");
1802
+ const title = document.getElementById("updatePanelTitle");
1803
+ const detail = document.getElementById("updatePanelDetail");
1804
+ const command = document.getElementById("updatePanelCommand");
1805
+ const versionStatus = config && config.versionStatus ? config.versionStatus : null;
1806
+
1807
+ if (!versionStatus || !versionStatus.needsUpdate || !versionStatus.latestVersion) {
1808
+ panel.classList.remove("is-visible");
1809
+ return;
1810
+ }
1811
+
1812
+ title.textContent = "\u53D1\u73B0\u65B0\u7248\u672C\u53EF\u66F4\u65B0";
1813
+ detail.textContent = "\u5F53\u524D\u7248\u672C " + versionStatus.currentVersion + "\uFF0C\u6700\u65B0\u7248\u672C "
1814
+ + 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";
1815
+ command.textContent = "npm install -g " + versionStatus.packageName + "@latest";
1816
+ panel.classList.add("is-visible");
1817
+ }
1818
+
1499
1819
  function supportsImageGeneration(profile) {
1500
1820
  return getPlanType(profile) !== "free";
1501
1821
  }
@@ -1547,6 +1867,23 @@ function renderAdminPage() {
1547
1867
  return local.slice(0, 2) + "***" + local.slice(-1) + "@" + domain;
1548
1868
  }
1549
1869
 
1870
+ function maskIdentifier(value) {
1871
+ if (typeof value !== "string" || !value.trim()) {
1872
+ return value || "";
1873
+ }
1874
+
1875
+ const trimmed = value.trim();
1876
+ const parts = trimmed.split(":");
1877
+ const suffix = parts.length > 1 ? parts.pop() || "" : trimmed;
1878
+ const prefix = parts.length > 0 ? parts.join(":") + ":" : "";
1879
+
1880
+ if (suffix.length <= 10) {
1881
+ return prefix + suffix.slice(0, 2) + "****";
1882
+ }
1883
+
1884
+ return prefix + suffix.slice(0, 5) + "****" + suffix.slice(-5);
1885
+ }
1886
+
1550
1887
  function getProfileDisplayLabel(profile) {
1551
1888
  if (!profile) {
1552
1889
  return "\u672A\u767B\u5F55";
@@ -1554,11 +1891,12 @@ function renderAdminPage() {
1554
1891
  if (profile.email) {
1555
1892
  return state.showEmails ? profile.email : maskEmail(profile.email);
1556
1893
  }
1557
- return profile.accountId || profile.profileId || "\u672A\u547D\u540D\u8D26\u53F7";
1894
+ const fallback = profile.accountId || profile.profileId || "\u672A\u547D\u540D\u8D26\u53F7";
1895
+ return state.showEmails ? fallback : maskIdentifier(fallback);
1558
1896
  }
1559
1897
 
1560
1898
  function updateEmailToggleButton() {
1561
- toggleEmailBtn.textContent = state.showEmails ? "\u9690\u85CF\u90AE\u7BB1" : "\u67E5\u770B\u90AE\u7BB1";
1899
+ toggleEmailBtn.textContent = state.showEmails ? "\u660E\u6587\u6A21\u5F0F" : "\u8131\u654F\u6A21\u5F0F";
1562
1900
  }
1563
1901
 
1564
1902
  function getProfileInitial(profile) {
@@ -1573,7 +1911,8 @@ function renderAdminPage() {
1573
1911
  if (item.accountEmail) {
1574
1912
  return state.showEmails ? item.accountEmail : maskEmail(item.accountEmail);
1575
1913
  }
1576
- return item.accountFallback || item.account || "\u672A\u767B\u5F55";
1914
+ const fallback = item.accountFallback || item.account || "\u672A\u767B\u5F55";
1915
+ return state.showEmails ? fallback : maskIdentifier(fallback);
1577
1916
  }
1578
1917
 
1579
1918
  function getPrimaryUsage(profile) {
@@ -1590,16 +1929,6 @@ function renderAdminPage() {
1590
1929
  return Math.max(0, Math.min(100, value));
1591
1930
  }
1592
1931
 
1593
- function getEstimatedLatencySeconds(profile) {
1594
- if (!profile || !profile.quota) {
1595
- return 0;
1596
- }
1597
- const primary = getPrimaryUsage(profile);
1598
- const secondary = getSecondaryUsage(profile);
1599
- const baseline = 0.62 + primary / 110 + secondary / 260;
1600
- return Math.round(baseline * 100) / 100;
1601
- }
1602
-
1603
1932
  function getProfileHealth(profile) {
1604
1933
  const now = Date.now();
1605
1934
  if (profile && profile.expiresAt && profile.expiresAt <= now) {
@@ -1655,10 +1984,31 @@ function renderAdminPage() {
1655
1984
  return "\u672A\u77E5";
1656
1985
  }
1657
1986
 
1658
- function formatQuotaValue(percent, profile, slot) {
1987
+ function getQuotaWindowLabel(profile, slot) {
1659
1988
  const quota = profile && profile.quota ? profile.quota : null;
1989
+ if (!quota) {
1990
+ return "\u672A\u540C\u6B65\u989D\u5EA6";
1991
+ }
1660
1992
  const field = slot === "primary" ? "primaryWindowMinutes" : "secondaryWindowMinutes";
1661
- return Math.round(percent) + "% \xB7 " + formatWindowLabel(quota && quota[field]);
1993
+ return formatWindowLabel(quota && quota[field]);
1994
+ }
1995
+
1996
+ function formatQuotaUsage(percent, profile, slot) {
1997
+ if (!profile || !profile.quota) {
1998
+ return "\u7B49\u5F85\u5237\u65B0";
1999
+ }
2000
+
2001
+ const used = Math.round(percent);
2002
+ const remaining = Math.max(0, 100 - used);
2003
+ return getQuotaWindowLabel(profile, slot) + " \xB7 \u5DF2\u7528 " + used + "% / \u5269\u4F59 " + remaining + "%";
2004
+ }
2005
+
2006
+ function formatQuotaRemaining(percent, profile) {
2007
+ if (!profile || !profile.quota) {
2008
+ return "--";
2009
+ }
2010
+
2011
+ return "\u5269\u4F59 " + String(Math.max(0, 100 - Math.round(percent))) + "%";
1662
2012
  }
1663
2013
 
1664
2014
  function createTimingTracker() {
@@ -1722,9 +2072,37 @@ function renderAdminPage() {
1722
2072
  return result;
1723
2073
  }
1724
2074
 
2075
+ function setTesterResultTab(tab) {
2076
+ state.testerResultTab = tab;
2077
+ document.querySelectorAll("[data-result-tab]").forEach(function (button) {
2078
+ button.classList.toggle("is-active", button.getAttribute("data-result-tab") === tab);
2079
+ });
2080
+ document.querySelectorAll("[data-result-panel]").forEach(function (panel) {
2081
+ panel.classList.toggle("is-active", panel.getAttribute("data-result-panel") === tab);
2082
+ });
2083
+ }
2084
+
2085
+ function openImagePreviewModal(src, meta, filename) {
2086
+ previewModalImage.src = src;
2087
+ previewModalMeta.textContent = meta || "\u56FE\u7247\u9884\u89C8";
2088
+ downloadPreviewBtn.href = src;
2089
+ downloadPreviewBtn.download = filename || "generated-image.png";
2090
+ imagePreviewModal.classList.add("is-open");
2091
+ imagePreviewModal.setAttribute("aria-hidden", "false");
2092
+ }
2093
+
2094
+ function closeImagePreviewModal() {
2095
+ imagePreviewModal.classList.remove("is-open");
2096
+ imagePreviewModal.setAttribute("aria-hidden", "true");
2097
+ previewModalImage.src = "";
2098
+ previewModalMeta.textContent = "\u7B49\u5F85\u56FE\u7247\u7ED3\u679C\u2026";
2099
+ downloadPreviewBtn.href = "#";
2100
+ downloadPreviewBtn.download = "generated-image.png";
2101
+ }
2102
+
1725
2103
  function clearPreview() {
1726
- responsePreview.hidden = true;
1727
2104
  responsePreview.innerHTML = "";
2105
+ responsePreviewEmpty.hidden = false;
1728
2106
  }
1729
2107
 
1730
2108
  function renderPreview(payload) {
@@ -1755,9 +2133,6 @@ function renderAdminPage() {
1755
2133
  const image = document.createElement("img");
1756
2134
  image.alt = "\u751F\u6210\u56FE\u7247 " + String(index + 1);
1757
2135
  image.src = "data:" + mime + ";base64," + item.b64_json;
1758
- card.appendChild(image);
1759
-
1760
- const caption = document.createElement("figcaption");
1761
2136
  const lines = [
1762
2137
  "\u56FE\u7247 " + String(index + 1),
1763
2138
  payload.size ? "\u5C3A\u5BF8: " + payload.size : "",
@@ -1765,12 +2140,36 @@ function renderAdminPage() {
1765
2140
  item.b64_json ? "base64 \u957F\u5EA6: " + String(item.b64_json.length) : "",
1766
2141
  item.revised_prompt ? "\u91CD\u5199\u63D0\u793A\u8BCD: " + item.revised_prompt : "",
1767
2142
  ].filter(Boolean);
1768
- caption.textContent = lines.join(" | ");
2143
+ const captionText = lines.join(" | ");
2144
+ image.addEventListener("click", function () {
2145
+ openImagePreviewModal(
2146
+ image.src,
2147
+ captionText,
2148
+ "generated-image-" + String(index + 1) + "." + format,
2149
+ );
2150
+ });
2151
+ card.appendChild(image);
2152
+
2153
+ const caption = document.createElement("figcaption");
2154
+ caption.textContent = captionText;
1769
2155
  card.appendChild(caption);
1770
2156
 
1771
2157
  const actions = document.createElement("div");
1772
2158
  actions.className = "preview-actions";
1773
2159
 
2160
+ const view = document.createElement("button");
2161
+ view.type = "button";
2162
+ view.className = "btn-secondary";
2163
+ view.textContent = "\u67E5\u770B\u5927\u56FE";
2164
+ view.addEventListener("click", function () {
2165
+ openImagePreviewModal(
2166
+ image.src,
2167
+ captionText,
2168
+ "generated-image-" + String(index + 1) + "." + format,
2169
+ );
2170
+ });
2171
+ actions.appendChild(view);
2172
+
1774
2173
  const download = document.createElement("a");
1775
2174
  download.href = image.src;
1776
2175
  download.download = "generated-image-" + String(index + 1) + "." + format;
@@ -1781,7 +2180,7 @@ function renderAdminPage() {
1781
2180
  responsePreview.appendChild(card);
1782
2181
  });
1783
2182
 
1784
- responsePreview.hidden = false;
2183
+ responsePreviewEmpty.hidden = true;
1785
2184
  }
1786
2185
 
1787
2186
  function buildExample(endpoint) {
@@ -1844,6 +2243,12 @@ function renderAdminPage() {
1844
2243
  detail: "\u672A\u663E\u5F0F\u6307\u5B9A model \u65F6\u751F\u6548",
1845
2244
  compact: true,
1846
2245
  },
2246
+ {
2247
+ label: "\u5F53\u524D\u7248\u672C",
2248
+ value: getVersionValue(config),
2249
+ detail: getVersionDetail(config),
2250
+ compact: true,
2251
+ },
1847
2252
  {
1848
2253
  label: "\u4ECA\u65E5\u8BF7\u6C42\u6570",
1849
2254
  value: String(requests.length),
@@ -1913,9 +2318,9 @@ function renderAdminPage() {
1913
2318
 
1914
2319
  filtered.sort(function (a, b) {
1915
2320
  if (sort === "latency-asc") {
1916
- const aLatency = getEstimatedLatencySeconds(a) || 9999;
1917
- const bLatency = getEstimatedLatencySeconds(b) || 9999;
1918
- return aLatency - bLatency;
2321
+ const aCapturedAt = getQuotaSnapshotTime(a) || 0;
2322
+ const bCapturedAt = getQuotaSnapshotTime(b) || 0;
2323
+ return bCapturedAt - aCapturedAt;
1919
2324
  }
1920
2325
  if (sort === "expiry-asc") {
1921
2326
  const aExpiry = a.expiresAt || Number.MAX_SAFE_INTEGER;
@@ -1934,6 +2339,16 @@ function renderAdminPage() {
1934
2339
  function renderProfiles(config) {
1935
2340
  const container = document.getElementById("profileList");
1936
2341
  const profiles = getFilteredProfiles(config);
2342
+ const gridClass = profiles.length <= 0
2343
+ ? ""
2344
+ : profiles.length === 1
2345
+ ? "profile-count-1"
2346
+ : profiles.length === 2
2347
+ ? "profile-count-2"
2348
+ : profiles.length === 3
2349
+ ? "profile-count-3"
2350
+ : "profile-count-many";
2351
+ container.className = gridClass ? "account-grid " + gridClass : "account-grid";
1937
2352
 
1938
2353
  if (profiles.length === 0) {
1939
2354
  container.innerHTML = '<div class="empty-state">\u5F53\u524D\u7B5B\u9009\u6761\u4EF6\u4E0B\u6CA1\u6709\u5339\u914D\u8D26\u53F7\u3002\u4F60\u53EF\u4EE5\u6E05\u7A7A\u7B5B\u9009\uFF0C\u6216\u8005\u5148\u65B0\u589E\u4E00\u4E2A\u8D26\u53F7\u3002</div>';
@@ -1941,6 +2356,7 @@ function renderAdminPage() {
1941
2356
  }
1942
2357
 
1943
2358
  container.innerHTML = profiles.map(function (profile) {
2359
+ const isSingleProfile = profiles.length === 1;
1944
2360
  const health = getProfileHealth(profile);
1945
2361
  const planType = getPlanType(profile);
1946
2362
  const imageCapability = getImageCapability(profile);
@@ -1949,7 +2365,9 @@ function renderAdminPage() {
1949
2365
  const primaryClass = health.barClass || "blue";
1950
2366
  const secondaryClass = secondary >= 85 ? "orange" : "blue";
1951
2367
  const actionButton = profile.isActive
1952
- ? '<button class="btn-secondary" type="button" disabled>\u5F53\u524D\u4F7F\u7528\u4E2D</button>'
2368
+ ? (isSingleProfile
2369
+ ? '<span class="account-status">\u5F53\u524D\u4F7F\u7528\u4E2D</span>'
2370
+ : '<button class="btn-secondary" type="button" disabled>\u5F53\u524D\u4F7F\u7528\u4E2D</button>')
1953
2371
  : '<button class="btn-secondary" type="button" data-profile-action="activate" data-profile-id="' + escapeHtml(profile.profileId) + '">\u5207\u6362</button>';
1954
2372
 
1955
2373
  return ""
@@ -1970,11 +2388,11 @@ function renderAdminPage() {
1970
2388
  + "</div>"
1971
2389
  + '<div class="account-metrics">'
1972
2390
  + '<div class="quota-row">'
1973
- + '<div class="quota-line"><span>' + escapeHtml(formatQuotaValue(primary, profile, "primary")) + '</span><strong>' + escapeHtml(Math.round(primary) + "%") + "</strong></div>"
2391
+ + '<div class="quota-line"><span>' + escapeHtml(formatQuotaUsage(primary, profile, "primary")) + '</span><strong>' + escapeHtml(formatQuotaRemaining(primary, profile)) + "</strong></div>"
1974
2392
  + '<div class="progress-track"><div class="progress-bar ' + escapeHtml(primaryClass) + '" style="width:' + escapeHtml(String(primary)) + '%"></div></div>'
1975
2393
  + "</div>"
1976
2394
  + '<div class="quota-row">'
1977
- + '<div class="quota-line"><span>' + escapeHtml(formatQuotaValue(secondary, profile, "secondary")) + '</span><strong>' + escapeHtml(Math.round(secondary) + "%") + "</strong></div>"
2395
+ + '<div class="quota-line"><span>' + escapeHtml(formatQuotaUsage(secondary, profile, "secondary")) + '</span><strong>' + escapeHtml(formatQuotaRemaining(secondary, profile)) + "</strong></div>"
1978
2396
  + '<div class="progress-track"><div class="progress-bar ' + escapeHtml(secondaryClass) + '" style="width:' + escapeHtml(String(secondary)) + '%"></div></div>'
1979
2397
  + "</div>"
1980
2398
  + "</div>"
@@ -1982,8 +2400,9 @@ function renderAdminPage() {
1982
2400
  + '<div class="meta-item"><label>\u5957\u9910</label><strong>' + escapeHtml(planType) + "</strong></div>"
1983
2401
  + '<div class="meta-item"><label>\u751F\u56FE\u80FD\u529B</label><strong>' + escapeHtml(imageCapability.detail) + "</strong></div>"
1984
2402
  + '<div class="meta-item"><label>\u91CD\u7F6E\u65F6\u95F4</label><strong>' + escapeHtml(describeReset(profile, "primary")) + "</strong></div>"
1985
- + '<div class="meta-item"><label>\u6700\u8FD1\u8BF7\u6C42\u8017\u65F6</label><strong>' + escapeHtml(profile.quota && typeof profile.quota.capturedAt === "number" ? formatRequestSeconds(getEstimatedLatencySeconds(profile)) : "--") + "</strong></div>"
1986
- + '<div class="meta-item"><label>Account ID</label><code>' + escapeHtml(profile.accountId || "\u672A\u63D0\u4F9B") + "</code></div>"
2403
+ + '<div class="meta-item"><label>\u989D\u5EA6\u5FEB\u7167</label><strong>' + escapeHtml(describeQuotaSnapshot(profile)) + "</strong></div>"
2404
+ + '<div class="meta-item"><label>\u989D\u5EA6\u9650\u5236</label><strong>' + escapeHtml(describeQuotaLimit(profile)) + "</strong></div>"
2405
+ + '<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>"
1987
2406
  + '<div class="meta-item"><label>\u8FC7\u671F\u65F6\u95F4</label><span>' + escapeHtml(formatTime(profile.expiresAt)) + "</span></div>"
1988
2407
  + "</div>"
1989
2408
  + '<div class="account-actions">'
@@ -2148,6 +2567,7 @@ function renderAdminPage() {
2148
2567
  ["Base URL", config.baseUrl],
2149
2568
  ["Provider", config.status.activeProvider || "openai-codex"],
2150
2569
  ["\u9ED8\u8BA4\u6A21\u578B", config.settings.defaultModel],
2570
+ ["\u5F53\u524D\u7248\u672C", getVersionValue(config)],
2151
2571
  ["\u5F53\u524D\u5957\u9910", config.profile ? getPlanType(config.profile) : "\u672A\u767B\u5F55"],
2152
2572
  ["\u751F\u56FE\u80FD\u529B", getImageCapability(config.profile).detail],
2153
2573
  ["\u8D26\u53F7\u72B6\u6001", loggedIn ? "\u5DF2\u767B\u5F55" : "\u672A\u767B\u5F55"],
@@ -2162,19 +2582,19 @@ function renderAdminPage() {
2162
2582
  }).join("");
2163
2583
 
2164
2584
  const endpointRows = [
2165
- ["\u7BA1\u7406\u9875\u5730\u5740", config.adminUrl],
2166
2585
  ["API Base URL", config.baseUrl],
2167
- ["\u517C\u5BB9\u63A5\u53E3", config.supportedEndpoints.map(function (item) { return item.path; }).join("\uFF0C")],
2168
2586
  ["\u5F53\u524D\u8D26\u53F7", getProfileDisplayLabel(config.profile)],
2587
+ ["\u9ED8\u8BA4\u6A21\u578B", config.settings.defaultModel],
2588
+ ["\u7248\u672C\u72B6\u6001", getVersionDetail(config)],
2169
2589
  ["\u5F53\u524D\u5957\u9910", config.profile ? getPlanType(config.profile) : "\u672A\u767B\u5F55"],
2170
2590
  ["\u751F\u56FE\u80FD\u529B", getImageCapability(config.profile).detail],
2171
- ["Access Token", config.profile ? config.profile.accessTokenPreview : "\u672A\u767B\u5F55"],
2172
- ["Refresh Token", config.profile ? config.profile.refreshTokenPreview : "\u672A\u767B\u5F55"],
2591
+ ["\u517C\u5BB9\u63A5\u53E3", config.supportedEndpoints.map(function (item) { return item.path; }).join("\uFF0C")],
2592
+ ["\u4EE4\u724C\u9884\u89C8", config.profile ? config.profile.accessTokenPreview : "\u672A\u767B\u5F55"],
2173
2593
  ];
2174
2594
 
2175
2595
  endpointInfo.innerHTML = endpointRows.map(function (row) {
2176
2596
  return ""
2177
- + '<div class="service-row">'
2597
+ + '<div class="service-row compact">'
2178
2598
  + "<label>" + escapeHtml(row[0]) + "</label>"
2179
2599
  + '<code class="mono">' + escapeHtml(row[1]) + "</code>"
2180
2600
  + "</div>";
@@ -2201,6 +2621,25 @@ function renderAdminPage() {
2201
2621
  }).join("");
2202
2622
  }
2203
2623
 
2624
+ function renderModelCatalogStatus(config) {
2625
+ const hint = document.getElementById("modelCatalogHint");
2626
+ if (!config || !config.modelCatalog) {
2627
+ hint.textContent = "\u6A21\u578B\u76EE\u5F55\u72B6\u6001\u672A\u77E5\u3002";
2628
+ return;
2629
+ }
2630
+
2631
+ const parts = [
2632
+ config.modelCatalog.source === "codex-cache" ? "\u5F53\u524D\u8BFB\u53D6 Codex \u672C\u5730\u6A21\u578B\u7F13\u5B58" : "\u5F53\u524D\u4F7F\u7528\u9879\u76EE\u5185\u7F6E\u6A21\u578B\u56DE\u9000\u5217\u8868",
2633
+ "\u5171 " + String(config.modelCatalog.modelCount) + " \u4E2A\u6A21\u578B",
2634
+ ];
2635
+
2636
+ if (config.modelCatalog.fetchedAt) {
2637
+ parts.push("Codex \u66F4\u65B0\u65F6\u95F4 " + new Date(config.modelCatalog.fetchedAt).toLocaleString("zh-CN", { hour12: false }));
2638
+ }
2639
+
2640
+ hint.textContent = parts.join("\uFF0C") + "\u3002";
2641
+ }
2642
+
2204
2643
  function syncHero(config) {
2205
2644
  const profileText = config.profile
2206
2645
  ? "\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"
@@ -2236,6 +2675,8 @@ function renderAdminPage() {
2236
2675
  renderOverview(config);
2237
2676
  renderProfiles(config);
2238
2677
  renderModelOptions(config);
2678
+ renderModelCatalogStatus(config);
2679
+ renderUpdatePanel(config);
2239
2680
  renderEndpoints(config);
2240
2681
  renderServiceInfo(config);
2241
2682
  renderTrend(config);
@@ -2287,23 +2728,76 @@ function renderAdminPage() {
2287
2728
  return body;
2288
2729
  }
2289
2730
 
2290
- async function refreshConfig() {
2291
- testerMeta.textContent = "\u5237\u65B0\u7BA1\u7406\u72B6\u6001";
2292
- const config = await fetchJson("/_gateway/admin/config");
2731
+ async function refreshConfig(options) {
2732
+ const syncRuntime = !!(options && options.syncRuntime);
2733
+ const silent = !!(options && options.silent);
2734
+ const url = syncRuntime ? "/_gateway/admin/runtime-refresh" : "/_gateway/admin/config";
2735
+ const requestOptions = syncRuntime ? { method: "POST" } : undefined;
2736
+
2737
+ if (!silent) {
2738
+ testerMeta.textContent = syncRuntime ? "\u540C\u6B65\u989D\u5EA6\u4E0E\u7248\u672C\u72B6\u6001" : "\u5237\u65B0\u7BA1\u7406\u72B6\u6001";
2739
+ }
2740
+
2741
+ const config = await fetchJson(url, requestOptions);
2293
2742
  renderConfig(config);
2294
- testerMeta.textContent = "\u51C6\u5907\u5C31\u7EEA";
2743
+
2744
+ if (!silent) {
2745
+ testerMeta.textContent = "\u51C6\u5907\u5C31\u7EEA";
2746
+ }
2747
+
2748
+ return config;
2749
+ }
2750
+
2751
+ function scheduleRuntimeRefresh() {
2752
+ window.setInterval(function () {
2753
+ if (document.hidden) {
2754
+ return;
2755
+ }
2756
+
2757
+ refreshConfig({
2758
+ syncRuntime: true,
2759
+ silent: true,
2760
+ }).catch(function (error) {
2761
+ console.warn("[admin] auto runtime refresh failed", error && error.message ? error.message : String(error));
2762
+ });
2763
+ }, RUNTIME_AUTO_REFRESH_MS);
2764
+ }
2765
+
2766
+ async function syncQuotaAfterProfileChange(config, sourceLabel) {
2767
+ if (!config || !config.profile || config.profile.quota) {
2768
+ return config;
2769
+ }
2770
+
2771
+ authStatus.textContent = "\u6B63\u5728\u540C\u6B65\u65B0\u8D26\u53F7\u7684\u989D\u5EA6\u4FE1\u606F...";
2772
+ try {
2773
+ const refreshedConfig = await fetchJson("/_gateway/admin/profiles/sync-quota", {
2774
+ method: "POST",
2775
+ });
2776
+ renderConfig(refreshedConfig);
2777
+ authStatus.textContent = refreshedConfig.profile && refreshedConfig.profile.quota
2778
+ ? sourceLabel + "\uFF0C\u989D\u5EA6\u4FE1\u606F\u5DF2\u540C\u6B65\u3002"
2779
+ : sourceLabel + "\uFF0C\u4F46\u6682\u672A\u83B7\u53D6\u5230\u989D\u5EA6\u5934\u4FE1\u606F\u3002";
2780
+ return refreshedConfig;
2781
+ } catch (error) {
2782
+ authStatus.textContent = sourceLabel + "\uFF0C\u4F46\u989D\u5EA6\u540C\u6B65\u5931\u8D25: " + (error && error.message ? error.message : String(error));
2783
+ return config;
2784
+ }
2295
2785
  }
2296
2786
 
2297
2787
  async function login() {
2298
2788
  const button = document.getElementById("loginBtn");
2299
2789
  setBusy(button, true);
2300
- authStatus.textContent = "\u6B63\u5728\u65B0\u589E\u8D26\u53F7\u5E76\u7B49\u5F85 OAuth \u5B8C\u6210...";
2790
+ authStatus.textContent = "\u6B63\u5728\u65B0\u589E\u8D26\u53F7\u3001\u7B49\u5F85 OAuth \u5B8C\u6210\uFF0C\u5E76\u540C\u6B65\u989D\u5EA6\u4FE1\u606F...";
2301
2791
  try {
2302
- const config = await fetchJson("/_gateway/admin/login", {
2792
+ let config = await fetchJson("/_gateway/admin/login", {
2303
2793
  method: "POST",
2304
2794
  });
2305
2795
  renderConfig(config);
2306
- authStatus.textContent = "\u8D26\u53F7\u5DF2\u4FDD\u5B58\uFF0C\u5E76\u5DF2\u5207\u6362\u4E3A\u5F53\u524D\u4F7F\u7528\u8D26\u53F7: " + getProfileDisplayLabel(config.profile);
2796
+ const baseMessage = "\u8D26\u53F7\u5DF2\u4FDD\u5B58\uFF0C\u5E76\u5DF2\u5207\u6362\u4E3A\u5F53\u524D\u4F7F\u7528\u8D26\u53F7: " + getProfileDisplayLabel(config.profile);
2797
+ config = await syncQuotaAfterProfileChange(config, baseMessage);
2798
+ if (config.profile && config.profile.quota) {
2799
+ authStatus.textContent = "\u8D26\u53F7\u5DF2\u4FDD\u5B58\uFF0C\u5DF2\u5207\u6362\u4E3A\u5F53\u524D\u4F7F\u7528\u8D26\u53F7\u5E76\u540C\u6B65\u989D\u5EA6\u4FE1\u606F: " + getProfileDisplayLabel(config.profile);
2800
+ }
2307
2801
  } catch (error) {
2308
2802
  authStatus.textContent = error.message;
2309
2803
  } finally {
@@ -2333,7 +2827,7 @@ function renderAdminPage() {
2333
2827
  setBusy(button, true);
2334
2828
  authStatus.textContent = action === "activate" ? "\u6B63\u5728\u5207\u6362\u5F53\u524D\u8D26\u53F7..." : "\u6B63\u5728\u5220\u9664\u8D26\u53F7...";
2335
2829
  try {
2336
- const config = await fetchJson(
2830
+ let config = await fetchJson(
2337
2831
  action === "activate" ? "/_gateway/admin/profiles/activate" : "/_gateway/admin/profiles/remove",
2338
2832
  {
2339
2833
  method: "POST",
@@ -2346,9 +2840,15 @@ function renderAdminPage() {
2346
2840
  },
2347
2841
  );
2348
2842
  renderConfig(config);
2349
- authStatus.textContent = action === "activate"
2350
- ? "\u5F53\u524D\u8D26\u53F7\u5DF2\u5207\u6362\u4E3A: " + getProfileDisplayLabel(config.profile)
2351
- : "\u8D26\u53F7\u5DF2\u5220\u9664\u3002";
2843
+ if (action === "activate") {
2844
+ const baseMessage = "\u5F53\u524D\u8D26\u53F7\u5DF2\u5207\u6362\u4E3A: " + getProfileDisplayLabel(config.profile);
2845
+ config = await syncQuotaAfterProfileChange(config, baseMessage);
2846
+ if (config.profile && config.profile.quota) {
2847
+ authStatus.textContent = baseMessage + "\uFF0C\u989D\u5EA6\u4FE1\u606F\u5DF2\u540C\u6B65\u3002";
2848
+ }
2849
+ } else {
2850
+ authStatus.textContent = "\u8D26\u53F7\u5DF2\u5220\u9664\u3002";
2851
+ }
2352
2852
  } catch (error) {
2353
2853
  authStatus.textContent = error.message;
2354
2854
  } finally {
@@ -2380,6 +2880,24 @@ function renderAdminPage() {
2380
2880
  }
2381
2881
  }
2382
2882
 
2883
+ async function refreshModels() {
2884
+ const button = document.getElementById("refreshModelsBtn");
2885
+ setBusy(button, true);
2886
+ authStatus.textContent = "\u6B63\u5728\u540C\u6B65 Codex \u6A21\u578B\u5217\u8868...";
2887
+ try {
2888
+ await fetchJson("/_gateway/models/refresh", {
2889
+ method: "POST",
2890
+ });
2891
+ const config = await fetchJson("/_gateway/admin/config");
2892
+ renderConfig(config);
2893
+ authStatus.textContent = "Codex \u6A21\u578B\u5217\u8868\u5DF2\u540C\u6B65\u3002";
2894
+ } catch (error) {
2895
+ authStatus.textContent = error.message;
2896
+ } finally {
2897
+ setBusy(button, false);
2898
+ }
2899
+ }
2900
+
2383
2901
  function extractModelFromPayload(payload) {
2384
2902
  if (!payload || typeof payload !== "object") {
2385
2903
  return state.config ? state.config.settings.defaultModel : "-";
@@ -2396,6 +2914,7 @@ function renderAdminPage() {
2396
2914
  const button = document.getElementById("runTestBtn");
2397
2915
  const tracker = createTimingTracker();
2398
2916
  setBusy(button, true);
2917
+ setTesterResultTab("response");
2399
2918
  testerMeta.textContent = "\u8BF7\u6C42\u4E2D: " + meta.method + " " + endpoint;
2400
2919
  responseBody.textContent = "\u8BF7\u6C42\u53D1\u9001\u4E2D...";
2401
2920
  timingBody.textContent = "\u8BF7\u6C42\u53D1\u9001\u4E2D...";
@@ -2440,6 +2959,10 @@ function renderAdminPage() {
2440
2959
  tracker,
2441
2960
  ["HTTP \u72B6\u6001: " + response.status + " " + response.statusText],
2442
2961
  );
2962
+ if (typeof parsed !== "string" && parsed && Array.isArray(parsed.data)
2963
+ && parsed.data.some(function (item) { return item && typeof item.b64_json === "string" && item.b64_json.length > 0; })) {
2964
+ setTesterResultTab("preview");
2965
+ }
2443
2966
  testerMeta.textContent = (response.ok ? "\u6210\u529F" : "\u5931\u8D25") + ": HTTP " + response.status + " " + meta.method + " " + endpoint;
2444
2967
 
2445
2968
  upsertRequestLog({
@@ -2495,12 +3018,28 @@ function renderAdminPage() {
2495
3018
  }
2496
3019
 
2497
3020
  document.getElementById("loginBtn").addEventListener("click", login);
2498
- document.getElementById("refreshBtn").addEventListener("click", refreshConfig);
3021
+ document.getElementById("refreshBtn").addEventListener("click", function () {
3022
+ authStatus.textContent = "\u6B63\u5728\u540C\u6B65\u989D\u5EA6\u4E0E\u7248\u672C\u72B6\u6001...";
3023
+ refreshConfig({
3024
+ syncRuntime: true,
3025
+ }).then(function () {
3026
+ authStatus.textContent = "\u989D\u5EA6\u4E0E\u7248\u672C\u72B6\u6001\u5DF2\u5237\u65B0\u3002";
3027
+ }).catch(function (error) {
3028
+ authStatus.textContent = error && error.message ? error.message : String(error);
3029
+ });
3030
+ });
2499
3031
  document.getElementById("logoutBtn").addEventListener("click", logout);
2500
3032
  contactBtn.addEventListener("click", openContactModal);
2501
3033
  document.getElementById("closeContactBtn").addEventListener("click", closeContactModal);
3034
+ document.getElementById("closeImagePreviewBtn").addEventListener("click", closeImagePreviewModal);
3035
+ document.getElementById("refreshModelsBtn").addEventListener("click", refreshModels);
2502
3036
  document.getElementById("saveModelBtn").addEventListener("click", saveModel);
2503
3037
  runTestBtn.addEventListener("click", runTest);
3038
+ document.querySelectorAll("[data-result-tab]").forEach(function (button) {
3039
+ button.addEventListener("click", function () {
3040
+ setTesterResultTab(button.getAttribute("data-result-tab"));
3041
+ });
3042
+ });
2504
3043
  toggleEmailBtn.addEventListener("click", function () {
2505
3044
  state.showEmails = !state.showEmails;
2506
3045
  updateEmailToggleButton();
@@ -2595,12 +3134,23 @@ function renderAdminPage() {
2595
3134
  }
2596
3135
  });
2597
3136
 
3137
+ imagePreviewModal.addEventListener("click", function (event) {
3138
+ if (event.target === imagePreviewModal) {
3139
+ closeImagePreviewModal();
3140
+ }
3141
+ });
3142
+
2598
3143
  document.addEventListener("keydown", function (event) {
3144
+ if (event.key === "Escape" && imagePreviewModal.classList.contains("is-open")) {
3145
+ closeImagePreviewModal();
3146
+ }
2599
3147
  if (event.key === "Escape" && contactModal.classList.contains("is-open")) {
2600
3148
  closeContactModal();
2601
3149
  }
2602
3150
  });
2603
3151
 
3152
+ setTesterResultTab(state.testerResultTab);
3153
+ scheduleRuntimeRefresh();
2604
3154
  refreshConfig().catch(function (error) {
2605
3155
  authStatus.textContent = error && error.message ? error.message : String(error);
2606
3156
  testerMeta.textContent = "\u52A0\u8F7D\u5931\u8D25";