codexmate 0.0.4 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/web-ui.html CHANGED
@@ -5,6 +5,7 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Codex Mate</title>
7
7
  <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
8
+ <script src="https://unpkg.com/json5@2/dist/index.min.js"></script>
8
9
  <style>
9
10
  @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&family=Source+Sans+3:wght@400;500;600&family=Space+Grotesk:wght@400;500;600;700&display=swap');
10
11
 
@@ -523,6 +524,101 @@
523
524
  gap: var(--spacing-xs);
524
525
  }
525
526
 
527
+ .recent-list {
528
+ display: flex;
529
+ flex-wrap: wrap;
530
+ gap: 8px;
531
+ margin-top: 8px;
532
+ }
533
+
534
+ .recent-item {
535
+ border: 1px solid var(--color-border-soft);
536
+ background: var(--color-surface-elevated);
537
+ border-radius: var(--radius-md);
538
+ padding: 10px 12px;
539
+ min-width: 170px;
540
+ text-align: left;
541
+ cursor: pointer;
542
+ transition: all var(--transition-fast) var(--ease-spring);
543
+ box-shadow: 0 2px 6px rgba(27, 23, 20, 0.06);
544
+ }
545
+
546
+ .recent-item:hover {
547
+ transform: translateY(-1px);
548
+ box-shadow: 0 4px 12px rgba(27, 23, 20, 0.12);
549
+ border-color: var(--color-border);
550
+ }
551
+
552
+ .recent-item:disabled {
553
+ opacity: 0.6;
554
+ cursor: not-allowed;
555
+ }
556
+
557
+ .recent-provider {
558
+ font-size: var(--font-size-body);
559
+ font-weight: var(--font-weight-secondary);
560
+ color: var(--color-text-primary);
561
+ margin-bottom: 4px;
562
+ }
563
+
564
+ .recent-model {
565
+ font-size: var(--font-size-caption);
566
+ color: var(--color-text-tertiary);
567
+ line-height: 1.4;
568
+ }
569
+
570
+ .recent-empty {
571
+ font-size: var(--font-size-caption);
572
+ color: var(--color-text-tertiary);
573
+ }
574
+
575
+ .health-report {
576
+ margin-top: 10px;
577
+ padding: 10px 12px;
578
+ border-radius: var(--radius-md);
579
+ border: 1px solid var(--color-border-soft);
580
+ background: var(--color-surface-alt);
581
+ display: grid;
582
+ gap: 8px;
583
+ }
584
+
585
+ .health-remote-toggle {
586
+ display: inline-flex;
587
+ align-items: center;
588
+ gap: 8px;
589
+ font-size: var(--font-size-caption);
590
+ color: var(--color-text-secondary);
591
+ }
592
+
593
+ .health-remote-toggle input {
594
+ accent-color: var(--color-brand);
595
+ }
596
+
597
+ .health-ok {
598
+ color: var(--color-success);
599
+ font-weight: var(--font-weight-secondary);
600
+ }
601
+
602
+ .health-issue {
603
+ background: #fff6f5;
604
+ border-left: 3px solid var(--color-error);
605
+ padding: 8px 10px;
606
+ border-radius: 10px;
607
+ }
608
+
609
+ .health-issue-title {
610
+ font-size: var(--font-size-caption);
611
+ font-weight: var(--font-weight-secondary);
612
+ color: var(--color-text-primary);
613
+ margin-bottom: 4px;
614
+ }
615
+
616
+ .health-issue-suggestion {
617
+ font-size: var(--font-size-caption);
618
+ color: var(--color-text-secondary);
619
+ line-height: 1.4;
620
+ }
621
+
526
622
  .btn-icon {
527
623
  width: 28px;
528
624
  height: 28px;
@@ -580,6 +676,31 @@
580
676
  box-shadow: var(--shadow-input-focus);
581
677
  }
582
678
 
679
+ .model-input {
680
+ width: 100%;
681
+ padding: 12px var(--spacing-sm);
682
+ border: 1px solid var(--color-border-soft);
683
+ border-radius: var(--radius-sm);
684
+ font-size: var(--font-size-body);
685
+ font-weight: var(--font-weight-body);
686
+ background-color: var(--color-surface-alt);
687
+ color: var(--color-text-primary);
688
+ outline: none;
689
+ transition: all var(--transition-fast) var(--ease-smooth);
690
+ box-shadow: inset 0 1px 2px rgba(31, 26, 23, 0.04);
691
+ }
692
+
693
+ .model-input:hover {
694
+ border-color: var(--color-border-strong);
695
+ background-color: var(--color-surface);
696
+ }
697
+
698
+ .model-input:focus {
699
+ background-color: var(--color-surface);
700
+ border-color: var(--color-brand);
701
+ box-shadow: var(--shadow-input-focus);
702
+ }
703
+
583
704
  .config-template-hint {
584
705
  margin-top: 8px;
585
706
  margin-bottom: 10px;
@@ -800,6 +921,51 @@
800
921
  letter-spacing: -0.01em;
801
922
  }
802
923
 
924
+ .btn-session-open {
925
+ border: 1px solid var(--color-border-soft);
926
+ border-radius: var(--radius-sm);
927
+ background: linear-gradient(to bottom, var(--color-surface) 0%, rgba(255, 255, 255, 0.9) 100%);
928
+ color: var(--color-text-secondary);
929
+ padding: 8px 12px;
930
+ font-size: var(--font-size-secondary);
931
+ font-weight: var(--font-weight-secondary);
932
+ cursor: pointer;
933
+ transition: all var(--transition-fast) var(--ease-spring);
934
+ white-space: nowrap;
935
+ box-shadow: var(--shadow-subtle);
936
+ letter-spacing: -0.01em;
937
+ }
938
+
939
+ .btn-session-clone {
940
+ border: 1px solid var(--color-border-soft);
941
+ border-radius: var(--radius-sm);
942
+ background: linear-gradient(to bottom, var(--color-surface) 0%, rgba(255, 255, 255, 0.9) 100%);
943
+ color: var(--color-text-secondary);
944
+ padding: 8px 12px;
945
+ font-size: var(--font-size-secondary);
946
+ font-weight: var(--font-weight-secondary);
947
+ cursor: pointer;
948
+ transition: all var(--transition-fast) var(--ease-spring);
949
+ white-space: nowrap;
950
+ box-shadow: var(--shadow-subtle);
951
+ letter-spacing: -0.01em;
952
+ }
953
+
954
+ .btn-session-delete {
955
+ border: 1px solid rgba(189, 70, 68, 0.4);
956
+ border-radius: var(--radius-sm);
957
+ background: linear-gradient(to bottom, rgba(255, 245, 245, 0.95) 0%, rgba(255, 255, 255, 0.9) 100%);
958
+ color: #b74545;
959
+ padding: 8px 12px;
960
+ font-size: var(--font-size-secondary);
961
+ font-weight: var(--font-weight-secondary);
962
+ cursor: pointer;
963
+ transition: all var(--transition-fast) var(--ease-spring);
964
+ white-space: nowrap;
965
+ box-shadow: var(--shadow-subtle);
966
+ letter-spacing: -0.01em;
967
+ }
968
+
803
969
  .btn-session-refresh {
804
970
  border: 1px solid var(--color-border-soft);
805
971
  border-radius: var(--radius-sm);
@@ -827,13 +993,39 @@
827
993
  transform: none;
828
994
  }
829
995
 
830
- .btn-session-export:hover {
996
+ .btn-session-export:hover,
997
+ .btn-session-open:hover {
998
+ border-color: var(--color-brand);
999
+ color: var(--color-brand);
1000
+ transform: translateY(-1px);
1001
+ }
1002
+
1003
+ .btn-session-export:disabled,
1004
+ .btn-session-open:disabled {
1005
+ opacity: 0.5;
1006
+ cursor: not-allowed;
1007
+ transform: none;
1008
+ }
1009
+
1010
+ .btn-session-clone:hover {
831
1011
  border-color: var(--color-brand);
832
1012
  color: var(--color-brand);
833
1013
  transform: translateY(-1px);
834
1014
  }
835
1015
 
836
- .btn-session-export:disabled {
1016
+ .btn-session-clone:disabled {
1017
+ opacity: 0.5;
1018
+ cursor: not-allowed;
1019
+ transform: none;
1020
+ }
1021
+
1022
+ .btn-session-delete:hover {
1023
+ border-color: rgba(189, 70, 68, 0.8);
1024
+ color: #9f3b3b;
1025
+ transform: translateY(-1px);
1026
+ }
1027
+
1028
+ .btn-session-delete:disabled {
837
1029
  opacity: 0.5;
838
1030
  cursor: not-allowed;
839
1031
  transform: none;
@@ -870,11 +1062,38 @@
870
1062
  min-height: 520px;
871
1063
  }
872
1064
 
873
- .session-list {
874
- display: flex;
875
- flex-direction: column;
876
- gap: var(--spacing-xs);
877
- position: sticky;
1065
+ .session-layout.session-standalone {
1066
+ grid-template-columns: minmax(0, 1fr);
1067
+ }
1068
+
1069
+ .session-standalone-page {
1070
+ max-width: 960px;
1071
+ margin: 0 auto;
1072
+ padding: var(--spacing-sm) 0;
1073
+ }
1074
+
1075
+ .session-standalone-title {
1076
+ font-size: var(--font-size-title);
1077
+ font-weight: var(--font-weight-title);
1078
+ color: var(--color-text-primary);
1079
+ margin-bottom: var(--spacing-sm);
1080
+ letter-spacing: -0.01em;
1081
+ }
1082
+
1083
+ .session-standalone-text {
1084
+ white-space: pre-wrap;
1085
+ font-family: var(--font-family-body);
1086
+ font-size: var(--font-size-body);
1087
+ line-height: 1.7;
1088
+ color: var(--color-text-primary);
1089
+ word-break: break-word;
1090
+ }
1091
+
1092
+ .session-list {
1093
+ display: flex;
1094
+ flex-direction: column;
1095
+ gap: var(--spacing-xs);
1096
+ position: sticky;
878
1097
  top: 12px;
879
1098
  height: 100%;
880
1099
  max-height: none;
@@ -988,6 +1207,21 @@
988
1207
  transition: all var(--transition-fast) var(--ease-spring);
989
1208
  }
990
1209
 
1210
+ .session-item-delete {
1211
+ border: 1px solid rgba(189, 70, 68, 0.35);
1212
+ background: rgba(189, 70, 68, 0.08);
1213
+ color: #b74545;
1214
+ width: 28px;
1215
+ height: 28px;
1216
+ border-radius: 8px;
1217
+ display: inline-flex;
1218
+ align-items: center;
1219
+ justify-content: center;
1220
+ cursor: pointer;
1221
+ flex-shrink: 0;
1222
+ transition: all var(--transition-fast) var(--ease-spring);
1223
+ }
1224
+
991
1225
  .session-item-copy:hover {
992
1226
  border-color: rgba(70, 86, 110, 0.7);
993
1227
  background: rgba(70, 86, 110, 0.16);
@@ -995,17 +1229,35 @@
995
1229
  transform: translateY(-1px);
996
1230
  }
997
1231
 
1232
+ .session-item-delete:hover {
1233
+ border-color: rgba(189, 70, 68, 0.7);
1234
+ background: rgba(189, 70, 68, 0.18);
1235
+ color: #9f3b3b;
1236
+ transform: translateY(-1px);
1237
+ }
1238
+
998
1239
  .session-item-copy:disabled {
999
1240
  opacity: 0.5;
1000
1241
  cursor: not-allowed;
1001
1242
  transform: none;
1002
1243
  }
1003
1244
 
1245
+ .session-item-delete:disabled {
1246
+ opacity: 0.5;
1247
+ cursor: not-allowed;
1248
+ transform: none;
1249
+ }
1250
+
1004
1251
  .session-item-copy svg {
1005
1252
  width: 16px;
1006
1253
  height: 16px;
1007
1254
  }
1008
1255
 
1256
+ .session-item-delete svg {
1257
+ width: 16px;
1258
+ height: 16px;
1259
+ }
1260
+
1009
1261
  .session-item-sub {
1010
1262
  font-size: var(--font-size-caption);
1011
1263
  color: var(--color-text-tertiary);
@@ -1280,6 +1532,37 @@
1280
1532
  }
1281
1533
  }
1282
1534
 
1535
+ @media (max-width: 520px) {
1536
+ .session-item-header {
1537
+ flex-direction: column;
1538
+ align-items: stretch;
1539
+ }
1540
+
1541
+ .session-item-actions {
1542
+ justify-content: flex-end;
1543
+ }
1544
+
1545
+ .session-actions {
1546
+ width: 100%;
1547
+ flex-direction: column;
1548
+ align-items: stretch;
1549
+ }
1550
+
1551
+ .btn-session-refresh,
1552
+ .btn-session-export {
1553
+ width: 100%;
1554
+ }
1555
+
1556
+ .session-toolbar-group.session-toolbar-actions {
1557
+ flex-direction: column;
1558
+ align-items: stretch;
1559
+ }
1560
+
1561
+ .session-toolbar-group.session-toolbar-actions .btn-tool {
1562
+ width: 100%;
1563
+ }
1564
+ }
1565
+
1283
1566
  .btn[disabled] {
1284
1567
  opacity: 0.5;
1285
1568
  cursor: not-allowed;
@@ -1310,6 +1593,9 @@
1310
1593
  background: linear-gradient(to bottom, var(--color-surface) 0%, rgba(255, 255, 255, 0.98) 100%);
1311
1594
  width: 90%;
1312
1595
  max-width: 400px;
1596
+ max-height: 90vh;
1597
+ overflow-y: auto;
1598
+ overscroll-behavior: contain;
1313
1599
  border-radius: var(--radius-lg);
1314
1600
  padding: var(--spacing-md);
1315
1601
  box-shadow: var(--shadow-modal);
@@ -1329,6 +1615,25 @@
1329
1615
  letter-spacing: -0.01em;
1330
1616
  }
1331
1617
 
1618
+ .modal-header {
1619
+ display: flex;
1620
+ align-items: center;
1621
+ justify-content: space-between;
1622
+ gap: var(--spacing-sm);
1623
+ margin-bottom: var(--spacing-md);
1624
+ flex-wrap: wrap;
1625
+ }
1626
+
1627
+ .modal-header .modal-title {
1628
+ margin-bottom: 0;
1629
+ }
1630
+
1631
+ .btn-modal-copy {
1632
+ padding: 6px 12px;
1633
+ white-space: nowrap;
1634
+ flex-shrink: 0;
1635
+ }
1636
+
1332
1637
  .form-group {
1333
1638
  margin-bottom: var(--spacing-sm);
1334
1639
  }
@@ -1404,39 +1709,313 @@
1404
1709
  opacity: 0.8;
1405
1710
  }
1406
1711
 
1407
- .btn-group {
1408
- display: flex;
1409
- gap: var(--spacing-sm);
1712
+ .quick-section {
1410
1713
  margin-top: var(--spacing-md);
1714
+ padding: var(--spacing-sm);
1715
+ border-radius: var(--radius-lg);
1716
+ border: 1px solid var(--color-border-soft);
1717
+ background: linear-gradient(140deg, rgba(255, 252, 247, 0.95), rgba(255, 255, 255, 0.6));
1411
1718
  }
1412
1719
 
1413
- .btn {
1414
- flex: 1;
1415
- padding: 14px var(--spacing-sm);
1416
- border-radius: var(--radius-sm);
1417
- font-size: var(--font-size-body);
1418
- font-weight: var(--font-weight-secondary);
1419
- cursor: pointer;
1420
- transition: all var(--transition-fast) var(--ease-spring);
1421
- border: 1px solid var(--color-border-soft);
1422
- background: linear-gradient(to bottom, var(--color-surface) 0%, rgba(255, 255, 255, 0.95) 100%);
1423
- color: var(--color-text-secondary);
1424
- box-shadow: var(--shadow-subtle);
1425
- letter-spacing: -0.01em;
1720
+ .quick-header {
1721
+ display: flex;
1722
+ flex-wrap: wrap;
1723
+ gap: var(--spacing-xs);
1724
+ align-items: flex-start;
1725
+ justify-content: space-between;
1726
+ margin-bottom: var(--spacing-sm);
1426
1727
  }
1427
1728
 
1428
- .btn:active {
1429
- transform: scale(0.97);
1729
+ .quick-title {
1730
+ font-size: var(--font-size-secondary);
1731
+ font-weight: var(--font-weight-secondary);
1732
+ color: var(--color-text-secondary);
1430
1733
  }
1431
1734
 
1432
- .btn-cancel {
1433
- background: linear-gradient(to bottom, var(--color-bg) 0%, rgba(247, 241, 232, 0.8) 100%);
1434
- color: var(--color-text-primary);
1435
- border: 1px solid var(--color-border-soft);
1735
+ .quick-actions {
1736
+ display: flex;
1737
+ flex-wrap: wrap;
1738
+ gap: var(--spacing-xs);
1436
1739
  }
1437
1740
 
1438
- .btn-cancel:hover {
1439
- background: linear-gradient(to bottom, var(--color-border) 0%, rgba(208, 196, 182, 0.5) 100%);
1741
+ .quick-steps {
1742
+ display: flex;
1743
+ flex-wrap: wrap;
1744
+ gap: var(--spacing-xs);
1745
+ margin-bottom: var(--spacing-sm);
1746
+ }
1747
+
1748
+ .quick-step {
1749
+ display: inline-flex;
1750
+ align-items: center;
1751
+ gap: 6px;
1752
+ padding: 4px 10px;
1753
+ border-radius: 999px;
1754
+ border: 1px dashed var(--color-border-soft);
1755
+ background: var(--color-surface);
1756
+ font-size: var(--font-size-caption);
1757
+ color: var(--color-text-secondary);
1758
+ }
1759
+
1760
+ .step-badge {
1761
+ width: 20px;
1762
+ height: 20px;
1763
+ border-radius: 999px;
1764
+ display: inline-flex;
1765
+ align-items: center;
1766
+ justify-content: center;
1767
+ background: var(--color-brand);
1768
+ color: #fff;
1769
+ font-size: 12px;
1770
+ font-weight: var(--font-weight-secondary);
1771
+ }
1772
+
1773
+ .quick-grid {
1774
+ display: grid;
1775
+ gap: var(--spacing-sm);
1776
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
1777
+ }
1778
+
1779
+ .quick-card {
1780
+ background: var(--color-surface);
1781
+ border: 1px solid var(--color-border-soft);
1782
+ border-radius: var(--radius-sm);
1783
+ padding: var(--spacing-sm);
1784
+ box-shadow: var(--shadow-subtle);
1785
+ }
1786
+
1787
+ .quick-option {
1788
+ display: flex;
1789
+ align-items: center;
1790
+ gap: 8px;
1791
+ font-size: var(--font-size-caption);
1792
+ color: var(--color-text-secondary);
1793
+ margin-bottom: 6px;
1794
+ }
1795
+
1796
+ .quick-option input {
1797
+ accent-color: var(--color-brand);
1798
+ }
1799
+
1800
+ .structured-section {
1801
+ margin-top: var(--spacing-md);
1802
+ padding: var(--spacing-sm);
1803
+ border-radius: var(--radius-lg);
1804
+ border: 1px solid var(--color-border-soft);
1805
+ background: rgba(255, 255, 255, 0.6);
1806
+ }
1807
+
1808
+ .structured-header {
1809
+ display: flex;
1810
+ flex-wrap: wrap;
1811
+ gap: var(--spacing-xs);
1812
+ align-items: baseline;
1813
+ justify-content: space-between;
1814
+ margin-bottom: var(--spacing-sm);
1815
+ }
1816
+
1817
+ .structured-title {
1818
+ font-size: var(--font-size-secondary);
1819
+ font-weight: var(--font-weight-secondary);
1820
+ color: var(--color-text-secondary);
1821
+ }
1822
+
1823
+ .structured-grid {
1824
+ display: grid;
1825
+ gap: var(--spacing-sm);
1826
+ grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
1827
+ }
1828
+
1829
+ .structured-card {
1830
+ background: var(--color-surface);
1831
+ border: 1px solid var(--color-border-soft);
1832
+ border-radius: var(--radius-sm);
1833
+ padding: var(--spacing-sm);
1834
+ box-shadow: var(--shadow-subtle);
1835
+ }
1836
+
1837
+ .structured-card-title {
1838
+ font-size: var(--font-size-body);
1839
+ font-weight: var(--font-weight-secondary);
1840
+ color: var(--color-text-secondary);
1841
+ margin-bottom: 8px;
1842
+ }
1843
+
1844
+ .provider-list {
1845
+ display: flex;
1846
+ flex-direction: column;
1847
+ gap: var(--spacing-xs);
1848
+ }
1849
+
1850
+ .provider-item {
1851
+ border: 1px dashed var(--color-border-soft);
1852
+ border-radius: var(--radius-sm);
1853
+ padding: var(--spacing-xs);
1854
+ background: var(--color-surface-alt);
1855
+ }
1856
+
1857
+ .provider-header {
1858
+ display: flex;
1859
+ flex-wrap: wrap;
1860
+ gap: var(--spacing-xs);
1861
+ align-items: center;
1862
+ margin-bottom: 6px;
1863
+ }
1864
+
1865
+ .provider-name {
1866
+ font-weight: var(--font-weight-secondary);
1867
+ color: var(--color-text-secondary);
1868
+ }
1869
+
1870
+ .provider-source {
1871
+ font-size: var(--font-size-caption);
1872
+ color: var(--color-text-tertiary);
1873
+ }
1874
+
1875
+ .provider-fields {
1876
+ display: grid;
1877
+ gap: 6px;
1878
+ }
1879
+
1880
+ .provider-field {
1881
+ display: flex;
1882
+ flex-wrap: wrap;
1883
+ gap: 6px;
1884
+ align-items: baseline;
1885
+ }
1886
+
1887
+ .provider-field-key {
1888
+ font-family: var(--font-family-mono);
1889
+ font-size: var(--font-size-caption);
1890
+ color: var(--color-text-muted);
1891
+ min-width: 110px;
1892
+ }
1893
+
1894
+ .provider-field-value {
1895
+ font-family: var(--font-family-mono);
1896
+ font-size: var(--font-size-caption);
1897
+ color: var(--color-text-secondary);
1898
+ word-break: break-all;
1899
+ }
1900
+
1901
+ .agent-list {
1902
+ display: flex;
1903
+ flex-direction: column;
1904
+ gap: var(--spacing-xs);
1905
+ }
1906
+
1907
+ .agent-item {
1908
+ border: 1px dashed var(--color-border-soft);
1909
+ border-radius: var(--radius-sm);
1910
+ padding: var(--spacing-xs);
1911
+ background: var(--color-surface-alt);
1912
+ }
1913
+
1914
+ .agent-header {
1915
+ display: flex;
1916
+ flex-wrap: wrap;
1917
+ gap: var(--spacing-xs);
1918
+ align-items: center;
1919
+ margin-bottom: 6px;
1920
+ }
1921
+
1922
+ .agent-name {
1923
+ font-weight: var(--font-weight-secondary);
1924
+ color: var(--color-text-secondary);
1925
+ }
1926
+
1927
+ .agent-id {
1928
+ font-size: var(--font-size-caption);
1929
+ color: var(--color-text-tertiary);
1930
+ }
1931
+
1932
+ .agent-meta {
1933
+ display: flex;
1934
+ flex-wrap: wrap;
1935
+ gap: 8px;
1936
+ font-size: var(--font-size-caption);
1937
+ color: var(--color-text-secondary);
1938
+ }
1939
+
1940
+ .list-row {
1941
+ display: flex;
1942
+ flex-wrap: wrap;
1943
+ gap: var(--spacing-xs);
1944
+ align-items: center;
1945
+ margin-bottom: var(--spacing-xs);
1946
+ }
1947
+
1948
+ .list-row:last-child {
1949
+ margin-bottom: 0;
1950
+ }
1951
+
1952
+ .list-row .form-input {
1953
+ flex: 1;
1954
+ min-width: 140px;
1955
+ }
1956
+
1957
+ .btn-mini {
1958
+ padding: 6px 10px;
1959
+ border-radius: var(--radius-sm);
1960
+ border: 1px solid var(--color-border-soft);
1961
+ background: linear-gradient(to bottom, rgba(255, 255, 255, 0.95) 0%, rgba(255, 255, 255, 0.85) 100%);
1962
+ font-size: var(--font-size-caption);
1963
+ font-weight: var(--font-weight-secondary);
1964
+ color: var(--color-text-secondary);
1965
+ cursor: pointer;
1966
+ transition: all var(--transition-fast) var(--ease-spring);
1967
+ box-shadow: var(--shadow-subtle);
1968
+ }
1969
+
1970
+ .btn-mini:hover {
1971
+ border-color: var(--color-brand);
1972
+ color: var(--color-brand);
1973
+ transform: translateY(-1px);
1974
+ }
1975
+
1976
+ .btn-mini.delete {
1977
+ color: var(--color-error);
1978
+ border-color: rgba(193, 72, 59, 0.35);
1979
+ }
1980
+
1981
+ .btn-mini.delete:hover {
1982
+ border-color: rgba(193, 72, 59, 0.7);
1983
+ color: var(--color-error);
1984
+ }
1985
+
1986
+ .btn-group {
1987
+ display: flex;
1988
+ gap: var(--spacing-sm);
1989
+ margin-top: var(--spacing-md);
1990
+ }
1991
+
1992
+ .btn {
1993
+ flex: 1;
1994
+ padding: 14px var(--spacing-sm);
1995
+ border-radius: var(--radius-sm);
1996
+ font-size: var(--font-size-body);
1997
+ font-weight: var(--font-weight-secondary);
1998
+ cursor: pointer;
1999
+ transition: all var(--transition-fast) var(--ease-spring);
2000
+ border: 1px solid var(--color-border-soft);
2001
+ background: linear-gradient(to bottom, var(--color-surface) 0%, rgba(255, 255, 255, 0.95) 100%);
2002
+ color: var(--color-text-secondary);
2003
+ box-shadow: var(--shadow-subtle);
2004
+ letter-spacing: -0.01em;
2005
+ }
2006
+
2007
+ .btn:active {
2008
+ transform: scale(0.97);
2009
+ }
2010
+
2011
+ .btn-cancel {
2012
+ background: linear-gradient(to bottom, var(--color-bg) 0%, rgba(247, 241, 232, 0.8) 100%);
2013
+ color: var(--color-text-primary);
2014
+ border: 1px solid var(--color-border-soft);
2015
+ }
2016
+
2017
+ .btn-cancel:hover {
2018
+ background: linear-gradient(to bottom, var(--color-border) 0%, rgba(208, 196, 182, 0.5) 100%);
1440
2019
  }
1441
2020
 
1442
2021
  .btn-confirm {
@@ -1662,27 +2241,27 @@
1662
2241
  <body>
1663
2242
  <div id="app" class="container" v-cloak>
1664
2243
  <!-- 主标题 -->
1665
- <h1 class="main-title">
2244
+ <h1 v-if="!sessionStandalone" class="main-title">
1666
2245
  Codex<br>
1667
2246
  <span class="accent">Mate.</span>
1668
2247
  </h1>
1669
- <p class="subtitle">本地配置中枢,统一管理 Codex / Claude Code / OpenClaw / 会话。</p>
2248
+ <p v-if="!sessionStandalone" class="subtitle">本地配置中枢,统一管理 Codex / Claude Code / OpenClaw / 会话。</p>
1670
2249
 
1671
2250
  <!-- 模式切换器 -->
1672
- <div class="segmented-control">
1673
- <button :class="['segment', { active: configMode === 'codex' }]" @click="switchConfigMode('codex')">
1674
- Codex 配置
1675
- </button>
1676
- <button :class="['segment', { active: configMode === 'claude' }]" @click="switchConfigMode('claude')">
1677
- Claude Code 配置
1678
- </button>
1679
- <button :class="['segment', { active: configMode === 'openclaw' }]" @click="switchConfigMode('openclaw')">
1680
- OpenClaw 配置
1681
- </button>
1682
- <button :class="['segment', { active: configMode === 'sessions' }]" @click="switchConfigMode('sessions')">
1683
- 会话浏览
1684
- </button>
1685
- </div>
2251
+ <div v-if="!sessionStandalone" class="segmented-control">
2252
+ <button :class="['segment', { active: configMode === 'codex' }]" @click="switchConfigMode('codex')">
2253
+ Codex 配置
2254
+ </button>
2255
+ <button :class="['segment', { active: configMode === 'claude' }]" @click="switchConfigMode('claude')">
2256
+ Claude Code 配置
2257
+ </button>
2258
+ <button :class="['segment', { active: configMode === 'openclaw' }]" @click="switchConfigMode('openclaw')">
2259
+ OpenClaw 配置
2260
+ </button>
2261
+ <button :class="['segment', { active: configMode === 'sessions' }]" @click="switchConfigMode('sessions')">
2262
+ 会话浏览
2263
+ </button>
2264
+ </div>
1686
2265
 
1687
2266
  <!-- 内容包裹器 - 稳定布局 -->
1688
2267
  <div class="content-wrapper">
@@ -1701,33 +2280,86 @@
1701
2280
  <div class="selector-header">
1702
2281
  <span class="selector-title">模型</span>
1703
2282
  <div class="selector-actions">
1704
- <button class="btn-icon" @click="showModelModal = true" title="添加模型">+</button>
1705
- <button class="btn-icon" @click="showModelListModal = true" title="管理模型">≡</button>
2283
+ <button class="btn-icon" @click="showModelModal = true" title="添加模型" v-if="modelsSource === 'legacy'">+</button>
2284
+ <button class="btn-icon" @click="showModelListModal = true" title="管理模型" v-if="modelsSource === 'legacy'">≡</button>
1706
2285
  </div>
1707
2286
  </div>
1708
- <select class="model-select" v-model="currentModel" @change="onModelChange">
1709
- <option v-for="model in models" :key="model" :value="model">{{ model }}</option>
2287
+ <select
2288
+ v-if="codexModelsLoading || modelsSource === 'remote'"
2289
+ class="model-select"
2290
+ v-model="currentModel"
2291
+ @change="onModelChange"
2292
+ :disabled="codexModelsLoading"
2293
+ >
2294
+ <option v-if="codexModelsLoading" value="">加载中...</option>
2295
+ <option v-else v-for="model in models" :key="model" :value="model">{{ model }}</option>
1710
2296
  </select>
2297
+ <input
2298
+ v-if="!codexModelsLoading && (modelsSource !== 'remote' || !modelsHasCurrent)"
2299
+ class="model-input"
2300
+ v-model="currentModel"
2301
+ @blur="onModelChange"
2302
+ placeholder="例如: gpt-5.3-codex"
2303
+ >
2304
+ <div class="config-template-hint" v-if="modelsSource === 'unlimited'">
2305
+ 当前提供商未提供模型列表,视为不限。模型可手动输入。
2306
+ </div>
2307
+ <div class="config-template-hint" v-if="modelsSource === 'error'">
2308
+ 模型列表获取失败,请检查接口或手动输入。
2309
+ </div>
2310
+ <div class="config-template-hint" v-if="modelsSource === 'remote' && !modelsHasCurrent">
2311
+ 当前模型不在接口列表中,请手动输入或在模板中调整。
2312
+ </div>
1711
2313
  <div class="config-template-hint">
1712
- Codex 配置改动采用模板确认模式:请先编辑模板,再手动确认应用。
2314
+ Codex 配置需先改模板,再手动应用。
1713
2315
  </div>
1714
2316
  <button class="btn-tool btn-template-editor" @click="openConfigTemplateEditor" :disabled="loading || !!initError">
1715
2317
  打开 Config 模板编辑器
1716
2318
  </button>
1717
2319
  </div>
1718
2320
 
2321
+ <div class="selector-section">
2322
+ <div class="selector-header">
2323
+ <span class="selector-title">最近使用</span>
2324
+ <span v-if="recentLoading" class="selector-title">加载中...</span>
2325
+ </div>
2326
+ <div v-if="recentConfigs.length === 0" class="recent-empty">
2327
+ 暂无记录
2328
+ </div>
2329
+ <div v-else class="recent-list">
2330
+ <button
2331
+ v-for="item in recentConfigs"
2332
+ :key="item.provider + '::' + item.model + '::' + (item.usedAt || '')"
2333
+ class="recent-item"
2334
+ @click="applyRecentConfig(item)"
2335
+ :disabled="loading || !!initError">
2336
+ <div class="recent-provider">{{ item.provider }}</div>
2337
+ <div class="recent-model">{{ item.model }}</div>
2338
+ </button>
2339
+ </div>
2340
+ </div>
2341
+
1719
2342
  <div class="selector-section">
1720
2343
  <div class="selector-header">
1721
2344
  <span class="selector-title">AGENTS.md</span>
1722
2345
  </div>
1723
2346
  <div class="config-template-hint">
1724
- 管理 Codex 指令文件,默认读写 <code>~/.codex/AGENTS.md</code>(与 <code>config.toml</code> 同级)。
2347
+ Codex 指令:<code>~/.codex/AGENTS.md</code>(同级 <code>config.toml</code>)。
1725
2348
  </div>
1726
2349
  <button class="btn-tool" @click="openAgentsEditor" :disabled="loading || !!initError || agentsLoading">
1727
2350
  {{ agentsLoading ? '加载中...' : '打开 AGENTS.md 编辑器' }}
1728
2351
  </button>
1729
2352
  </div>
1730
2353
 
2354
+ <div class="selector-section">
2355
+ <div class="selector-header">
2356
+ <span class="selector-title">配置健康检查</span>
2357
+ </div>
2358
+ <button class="btn-tool" @click="runHealthCheck" :disabled="healthCheckLoading || loading || !!initError">
2359
+ {{ healthCheckLoading ? '检查中...' : '运行检查' }}
2360
+ </button>
2361
+ </div>
2362
+
1731
2363
  <div v-if="!loading && !initError" class="card-list">
1732
2364
  <div v-for="provider in providersList" :key="provider.name"
1733
2365
  :class="['card', { active: currentProvider === provider.name }]"
@@ -1770,17 +2402,33 @@
1770
2402
  </div>
1771
2403
  </div>
1772
2404
 
1773
- <!-- Claude Code 配置模式 -->
1774
- <div v-show="configMode === 'claude'" class="mode-content">
1775
- <!-- 添加提供商按钮 -->
1776
- <button class="btn-add" @click="showClaudeConfigModal = true" v-if="!loading && !initError">
2405
+ <!-- Claude Code 配置模式 -->
2406
+ <div v-show="configMode === 'claude'" class="mode-content">
2407
+ <!-- 添加提供商按钮 -->
2408
+ <button class="btn-add" @click="openClaudeConfigModal" v-if="!loading && !initError">
1777
2409
  <svg class="icon" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2">
1778
2410
  <path d="M10 4v12M4 10h12"/>
1779
2411
  </svg>
1780
2412
  添加提供商
1781
2413
  </button>
1782
2414
  <div class="config-template-hint">
1783
- 默认应用到 <code>~/.claude/settings.json</code>;编辑弹窗中可使用兼容模式写入系统环境变量。
2415
+ 默认应用到 <code>~/.claude/settings.json</code>。
2416
+ </div>
2417
+
2418
+ <div class="selector-section">
2419
+ <div class="selector-header">
2420
+ <span class="selector-title">模型</span>
2421
+ </div>
2422
+ <input
2423
+ class="model-input"
2424
+ v-model="currentClaudeModel"
2425
+ @blur="onClaudeModelChange"
2426
+ @keyup.enter="onClaudeModelChange"
2427
+ placeholder="例如: claude-3-7-sonnet"
2428
+ >
2429
+ <div class="config-template-hint">
2430
+ 模型修改后会自动保存并应用到当前配置。
2431
+ </div>
1784
2432
  </div>
1785
2433
 
1786
2434
  <div class="card-list">
@@ -1814,73 +2462,106 @@
1814
2462
  </div>
1815
2463
  </div>
1816
2464
  </div>
1817
- </div>
1818
- </div>
1819
-
1820
- <!-- OpenClaw 配置模式 -->
1821
- <div v-show="configMode === 'openclaw'" class="mode-content">
1822
- <button class="btn-add" @click="openOpenclawAddModal" v-if="!loading && !initError">
1823
- <svg class="icon" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2">
1824
- <path d="M10 4v12M4 10h12"/>
1825
- </svg>
1826
- 添加 OpenClaw 配置
1827
- </button>
1828
- <div class="config-template-hint">
1829
- 默认应用到 <code>~/.openclaw/openclaw.json</code>;支持 JSON5(注释/尾逗号)。
1830
- </div>
1831
-
1832
- <div class="selector-section">
1833
- <div class="selector-header">
1834
- <span class="selector-title">AGENTS.md</span>
1835
- </div>
1836
- <div class="config-template-hint">
1837
- 管理 OpenClaw Workspace 指令文件,默认读写 <code>~/.openclaw/workspace/AGENTS.md</code>。
1838
- </div>
1839
- <button class="btn-tool" @click="openOpenclawAgentsEditor" :disabled="loading || !!initError || agentsLoading">
1840
- {{ agentsLoading ? '加载中...' : '打开 AGENTS.md 编辑器' }}
1841
- </button>
1842
- </div>
1843
-
1844
- <div class="card-list">
1845
- <div v-for="(config, name) in openclawConfigs" :key="name"
1846
- :class="['card', { active: currentOpenclawConfig === name }]"
1847
- @click="applyOpenclawConfig(name)">
1848
- <div class="card-leading">
1849
- <div class="card-icon">{{ name.charAt(0).toUpperCase() }}</div>
1850
- <div class="card-content">
1851
- <div class="card-title">{{ name }}</div>
1852
- <div class="card-subtitle">{{ openclawSubtitle(config) }}</div>
1853
- </div>
1854
- </div>
1855
- <div class="card-trailing">
1856
- <span :class="['pill', openclawHasContent(config) ? 'configured' : 'empty']">
1857
- {{ openclawHasContent(config) ? '已配置' : '未配置' }}
1858
- </span>
1859
- <div class="card-actions" @click.stop>
1860
- <button class="card-action-btn" @click="openOpenclawEditModal(name)" title="编辑">
1861
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1862
- <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
1863
- <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
1864
- </svg>
1865
- </button>
1866
- <button class="card-action-btn delete" @click="deleteOpenclawConfig(name)" title="删除">
1867
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1868
- <path d="M3 6h18"/>
1869
- <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
1870
- </svg>
1871
- </button>
1872
- </div>
1873
- </div>
1874
- </div>
1875
- </div>
1876
- </div>
1877
-
1878
- <!-- 会话浏览模式 -->
1879
- <div v-show="configMode === 'sessions'" class="mode-content">
2465
+ </div>
2466
+ </div>
2467
+
2468
+ <!-- OpenClaw 配置模式 -->
2469
+ <div v-show="configMode === 'openclaw'" class="mode-content">
2470
+ <button class="btn-add" @click="openOpenclawAddModal" v-if="!loading && !initError">
2471
+ <svg class="icon" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2">
2472
+ <path d="M10 4v12M4 10h12"/>
2473
+ </svg>
2474
+ 添加 OpenClaw 配置
2475
+ </button>
2476
+ <div class="config-template-hint">
2477
+ 默认应用到 <code>~/.openclaw/openclaw.json</code>。支持 JSON5(注释/尾逗号)。
2478
+ </div>
2479
+
2480
+ <div class="selector-section">
2481
+ <div class="selector-header">
2482
+ <span class="selector-title">AGENTS.md</span>
2483
+ </div>
2484
+ <div class="config-template-hint">
2485
+ 管理 OpenClaw Workspace 指令文件,默认读写 <code>~/.openclaw/workspace/AGENTS.md</code>。
2486
+ </div>
2487
+ <button class="btn-tool" @click="openOpenclawAgentsEditor" :disabled="loading || !!initError || agentsLoading">
2488
+ {{ agentsLoading ? '加载中...' : '打开 AGENTS.md 编辑器' }}
2489
+ </button>
2490
+ </div>
2491
+
1880
2492
  <div class="selector-section">
1881
2493
  <div class="selector-header">
1882
- <span class="selector-title">会话来源</span>
2494
+ <span class="selector-title">工作区文件</span>
2495
+ </div>
2496
+ <input
2497
+ class="form-input"
2498
+ v-model="openclawWorkspaceFileName"
2499
+ placeholder="例如: SOUL.md">
2500
+ <div class="config-template-hint">
2501
+ 仅支持 OpenClaw Workspace 内的 <code>.md</code> 文件。
2502
+ </div>
2503
+ <button class="btn-tool" @click="openOpenclawWorkspaceEditor" :disabled="loading || !!initError || agentsLoading">
2504
+ {{ agentsLoading ? '加载中...' : '打开工作区文件' }}
2505
+ </button>
2506
+ </div>
2507
+
2508
+ <div class="card-list">
2509
+ <div v-for="(config, name) in openclawConfigs" :key="name"
2510
+ :class="['card', { active: currentOpenclawConfig === name }]"
2511
+ @click="applyOpenclawConfig(name)">
2512
+ <div class="card-leading">
2513
+ <div class="card-icon">{{ name.charAt(0).toUpperCase() }}</div>
2514
+ <div class="card-content">
2515
+ <div class="card-title">{{ name }}</div>
2516
+ <div class="card-subtitle">{{ openclawSubtitle(config) }}</div>
2517
+ </div>
2518
+ </div>
2519
+ <div class="card-trailing">
2520
+ <span :class="['pill', openclawHasContent(config) ? 'configured' : 'empty']">
2521
+ {{ openclawHasContent(config) ? '已配置' : '未配置' }}
2522
+ </span>
2523
+ <div class="card-actions" @click.stop>
2524
+ <button class="card-action-btn" @click="openOpenclawEditModal(name)" title="编辑">
2525
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
2526
+ <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
2527
+ <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
2528
+ </svg>
2529
+ </button>
2530
+ <button class="card-action-btn delete" @click="deleteOpenclawConfig(name)" title="删除">
2531
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
2532
+ <path d="M3 6h18"/>
2533
+ <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
2534
+ </svg>
2535
+ </button>
2536
+ </div>
2537
+ </div>
1883
2538
  </div>
2539
+ </div>
2540
+ </div>
2541
+
2542
+ <!-- 会话浏览模式 -->
2543
+ <div v-show="configMode === 'sessions'" class="mode-content">
2544
+ <div v-if="sessionStandalone" class="session-standalone-page">
2545
+ <div v-if="sessionStandaloneLoading" class="state-message">
2546
+ 加载中...
2547
+ </div>
2548
+ <div v-else-if="sessionStandaloneError" class="state-message error">
2549
+ {{ sessionStandaloneError }}
2550
+ </div>
2551
+ <div v-else>
2552
+ <div class="session-standalone-title">
2553
+ {{ sessionStandaloneTitle }}
2554
+ <span v-if="sessionStandaloneSourceLabel"> · {{ sessionStandaloneSourceLabel }}</span>
2555
+ </div>
2556
+ <pre class="session-standalone-text">{{ sessionStandaloneText }}</pre>
2557
+ </div>
2558
+ </div>
2559
+
2560
+ <div v-else>
2561
+ <div v-if="!sessionStandalone" class="selector-section">
2562
+ <div class="selector-header">
2563
+ <span class="selector-title">会话来源</span>
2564
+ </div>
1884
2565
  <div class="session-toolbar">
1885
2566
  <div class="session-toolbar-group">
1886
2567
  <select class="session-source-select" v-model="sessionFilterSource" @change="onSessionSourceChange" :disabled="sessionsLoading">
@@ -1902,8 +2583,8 @@
1902
2583
  class="session-query-input"
1903
2584
  v-model="sessionQuery"
1904
2585
  @keyup.enter="loadSessions"
1905
- disabled
1906
- placeholder="关键词检索(暂时停用)">
2586
+ :disabled="sessionsLoading || !isSessionQueryEnabled"
2587
+ :placeholder="sessionQueryPlaceholder">
1907
2588
  </div>
1908
2589
  <div class="session-toolbar-group">
1909
2590
  <select
@@ -1937,20 +2618,22 @@
1937
2618
  </div>
1938
2619
  </div>
1939
2620
  <div class="session-hint">
1940
- 关键词检索与角色/时间筛选暂时停用;当前仅支持来源与路径筛选会话列表。右侧预览区仅支持查看与导出。
2621
+ 关键词检索仅 Codex 可用;<br>
2622
+ 角色/时间筛选暂不可用;<br>
2623
+ 仅支持来源与路径筛选,右侧仅查看/导出。
1941
2624
  </div>
1942
2625
  </div>
1943
2626
 
1944
- <div v-if="sessionsLoading" class="state-message">
2627
+ <div v-if="!sessionStandalone && sessionsLoading" class="state-message">
1945
2628
  会话加载中...
1946
2629
  </div>
1947
2630
 
1948
- <div v-else-if="sessionsList.length === 0" class="session-empty">
2631
+ <div v-else-if="!sessionStandalone && sessionsList.length === 0" class="session-empty">
1949
2632
  暂无可用会话记录
1950
2633
  </div>
1951
2634
 
1952
- <div v-else class="session-layout">
1953
- <div class="session-list">
2635
+ <div v-else :class="['session-layout', { 'session-standalone': sessionStandalone }]">
2636
+ <div v-if="!sessionStandalone" class="session-list">
1954
2637
  <div
1955
2638
  v-for="session in sessionsList"
1956
2639
  :key="session.source + '-' + session.sessionId + '-' + session.filePath"
@@ -1978,6 +2661,21 @@
1978
2661
  <path d="M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2"></path>
1979
2662
  </svg>
1980
2663
  </button>
2664
+ <button
2665
+ v-if="isDeleteAvailable(session)"
2666
+ class="session-item-delete"
2667
+ @click.stop="deleteSession(session)"
2668
+ :disabled="sessionsLoading || sessionDeleting[getSessionExportKey(session)]"
2669
+ aria-label="删除会话"
2670
+ title="删除会话">
2671
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
2672
+ <path d="M3 6h18"></path>
2673
+ <path d="M8 6V4h8v2"></path>
2674
+ <path d="M19 6l-1 14H6L5 6"></path>
2675
+ <path d="M10 11v6"></path>
2676
+ <path d="M14 11v6"></path>
2677
+ </svg>
2678
+ </button>
1981
2679
  </div>
1982
2680
  </div>
1983
2681
  <div class="session-item-meta">
@@ -2006,16 +2704,37 @@
2006
2704
  <span class="session-preview-meta-item">{{ activeSession.cwd }}</span>
2007
2705
  </div>
2008
2706
  </div>
2009
- <div class="session-actions">
2707
+ <div v-if="!sessionStandalone" class="session-actions">
2010
2708
  <button class="btn-session-refresh" @click="loadActiveSessionDetail" :disabled="sessionDetailLoading || !activeSession">
2011
2709
  {{ sessionDetailLoading ? '加载中...' : '刷新内容' }}
2012
2710
  </button>
2711
+ <button
2712
+ v-if="isCloneAvailable(activeSession)"
2713
+ class="btn-session-clone"
2714
+ @click="cloneSession(activeSession)"
2715
+ :disabled="!activeSession || sessionsLoading || sessionCloning[getSessionExportKey(activeSession)]">
2716
+ {{ (activeSession && sessionCloning[getSessionExportKey(activeSession)]) ? '克隆中...' : '克隆会话' }}
2717
+ </button>
2718
+ <button
2719
+ v-if="isDeleteAvailable(activeSession)"
2720
+ class="btn-session-delete"
2721
+ @click="deleteSession(activeSession)"
2722
+ :disabled="!activeSession || sessionsLoading || sessionDeleting[getSessionExportKey(activeSession)]">
2723
+ {{ (activeSession && sessionDeleting[getSessionExportKey(activeSession)]) ? '删除中...' : '删除会话' }}
2724
+ </button>
2013
2725
  <button
2014
2726
  class="btn-session-export"
2015
2727
  @click="exportSession(activeSession)"
2016
2728
  :disabled="!activeSession || sessionExporting[getSessionExportKey(activeSession)]">
2017
2729
  {{ (activeSession && sessionExporting[getSessionExportKey(activeSession)]) ? '导出中...' : '导出记录' }}
2018
2730
  </button>
2731
+ <button
2732
+ v-if="!sessionStandalone"
2733
+ class="btn-session-open"
2734
+ @click="openSessionStandalone(activeSession)"
2735
+ :disabled="!activeSession">
2736
+ 新页查看
2737
+ </button>
2019
2738
  </div>
2020
2739
  </div>
2021
2740
 
@@ -2051,12 +2770,14 @@
2051
2770
  </div>
2052
2771
  </template>
2053
2772
 
2054
- <div v-else class="session-preview-empty">
2055
- 请先在左侧选择一个会话
2056
- </div>
2057
- </div>
2058
- </div>
2059
- </div>
2773
+ <div v-else class="session-preview-empty">
2774
+ <span v-if="sessionStandaloneError">{{ sessionStandaloneError }}</span>
2775
+ <span v-else>请先在左侧选择一个会话</span>
2776
+ </div>
2777
+ </div>
2778
+ </div>
2779
+ </div>
2780
+ </div>
2060
2781
 
2061
2782
  <!-- 加载状态 -->
2062
2783
  <div v-if="loading" class="state-message">
@@ -2172,10 +2893,6 @@
2172
2893
  <label class="form-label">Base URL</label>
2173
2894
  <input v-model="newClaudeConfig.baseUrl" class="form-input" placeholder="https://open.bigmodel.cn/api/anthropic">
2174
2895
  </div>
2175
- <div class="form-group">
2176
- <label class="form-label">模型</label>
2177
- <input v-model="newClaudeConfig.model" class="form-input" placeholder="例如: claude-sonnet-4-20250514">
2178
- </div>
2179
2896
 
2180
2897
  <div class="btn-group">
2181
2898
  <button class="btn btn-cancel" @click="closeClaudeConfigModal">取消</button>
@@ -2185,9 +2902,9 @@
2185
2902
  </div>
2186
2903
 
2187
2904
  <!-- 编辑Claude配置模态框 -->
2188
- <div v-if="showEditConfigModal" class="modal-overlay" @click.self="closeEditConfigModal">
2189
- <div class="modal">
2190
- <div class="modal-title">编辑 Claude Code 配置</div>
2905
+ <div v-if="showEditConfigModal" class="modal-overlay" @click.self="closeEditConfigModal">
2906
+ <div class="modal">
2907
+ <div class="modal-title">编辑 Claude Code 配置</div>
2191
2908
 
2192
2909
  <div class="form-group">
2193
2910
  <label class="form-label">配置名称</label>
@@ -2201,71 +2918,292 @@
2201
2918
  <label class="form-label">Base URL</label>
2202
2919
  <input v-model="editingConfig.baseUrl" class="form-input" placeholder="https://open.bigmodel.cn/api/anthropic">
2203
2920
  </div>
2921
+
2922
+ <div class="btn-group">
2923
+ <button class="btn btn-cancel" @click="closeEditConfigModal">取消</button>
2924
+ <button class="btn btn-confirm" @click="saveAndApplyConfig">保存并应用</button>
2925
+ </div>
2926
+ </div>
2927
+ </div>
2928
+
2929
+ <div v-if="showOpenclawConfigModal" class="modal-overlay" @click.self="closeOpenclawConfigModal">
2930
+ <div class="modal modal-wide">
2931
+ <div class="modal-title">{{ openclawEditorTitle }}</div>
2932
+
2933
+ <div class="form-group">
2934
+ <label class="form-label">配置名称</label>
2935
+ <input v-model="openclawEditing.name" class="form-input" :readonly="openclawEditing.lockName" placeholder="例如: 默认配置">
2936
+ </div>
2937
+
2938
+ <div class="form-group">
2939
+ <label class="form-label">目标文件</label>
2940
+ <div class="form-hint">
2941
+ {{ openclawConfigPath || '未加载' }}
2942
+ <span v-if="openclawConfigPath">
2943
+ ({{ openclawConfigExists ? '已存在' : '不存在,将在应用时创建' }})
2944
+ </span>
2945
+ </div>
2946
+ <div class="btn-group" style="justify-content:flex-start;">
2947
+ <button class="btn btn-confirm secondary" @click="loadOpenclawConfigFromFile" :disabled="openclawFileLoading">
2948
+ {{ openclawFileLoading ? '加载中...' : '加载当前配置' }}
2949
+ </button>
2950
+ </div>
2951
+ </div>
2952
+
2953
+ <div class="quick-section">
2954
+ <div class="quick-header">
2955
+ <div>
2956
+ <div class="quick-title">新手快速配置</div>
2957
+ <div class="form-hint">按 3 步完成:填 Provider 和模型,写入编辑器,保存并应用。</div>
2958
+ </div>
2959
+ <div class="quick-actions">
2960
+ <button class="btn-mini" @click="syncOpenclawQuickFromText">从编辑器读取</button>
2961
+ <button class="btn-mini" @click="resetOpenclawQuick">清空</button>
2962
+ </div>
2963
+ </div>
2964
+ <div class="quick-steps">
2965
+ <div class="quick-step"><span class="step-badge">1</span><span>填写 Provider 与模型</span></div>
2966
+ <div class="quick-step"><span class="step-badge">2</span><span>点击写入编辑器</span></div>
2967
+ <div class="quick-step"><span class="step-badge">3</span><span>保存并应用</span></div>
2968
+ </div>
2969
+ <div class="quick-grid">
2970
+ <div class="quick-card">
2971
+ <div class="structured-card-title">Provider</div>
2972
+ <div class="form-group">
2973
+ <label class="form-label">Provider 名称</label>
2974
+ <input v-model="openclawQuick.providerName" class="form-input" placeholder="例如: custom-myapi">
2975
+ <div class="form-hint">会拼成 provider/model 作为主模型标识。</div>
2976
+ </div>
2977
+ <div class="form-group">
2978
+ <label class="form-label">Base URL</label>
2979
+ <input v-model="openclawQuick.baseUrl" class="form-input" placeholder="https://api.example.com/v1">
2980
+ </div>
2981
+ <div class="form-group">
2982
+ <label class="form-label">API Key</label>
2983
+ <div class="list-row">
2984
+ <input v-model="openclawQuick.apiKey" class="form-input" :type="openclawQuick.showKey ? 'text' : 'password'" placeholder="sk-...">
2985
+ <button class="btn-mini" @click="toggleOpenclawQuickKey">
2986
+ {{ openclawQuick.showKey ? '隐藏' : '显示' }}
2987
+ </button>
2988
+ </div>
2989
+ <div class="form-hint">留空表示不覆盖现有 key。</div>
2990
+ </div>
2991
+ <div class="form-group">
2992
+ <label class="form-label">API 类型</label>
2993
+ <input v-model="openclawQuick.apiType" class="form-input" list="openclawApiTypeList" placeholder="例如: openai-responses">
2994
+ <datalist id="openclawApiTypeList">
2995
+ <option value="openai-responses"></option>
2996
+ <option value="openai-chat"></option>
2997
+ <option value="anthropic"></option>
2998
+ <option value="custom"></option>
2999
+ </datalist>
3000
+ </div>
3001
+ </div>
3002
+
3003
+ <div class="quick-card">
3004
+ <div class="structured-card-title">模型</div>
3005
+ <div class="form-group">
3006
+ <label class="form-label">模型 ID</label>
3007
+ <input v-model="openclawQuick.modelId" class="form-input" placeholder="例如: gpt-4.1">
3008
+ </div>
3009
+ <div class="form-group">
3010
+ <label class="form-label">展示名称</label>
3011
+ <input v-model="openclawQuick.modelName" class="form-input" placeholder="留空则使用模型 ID">
3012
+ </div>
3013
+ <div class="form-group">
3014
+ <label class="form-label">上下文与最大输出</label>
3015
+ <div class="list-row">
3016
+ <input v-model="openclawQuick.contextWindow" class="form-input" placeholder="上下文长度">
3017
+ <input v-model="openclawQuick.maxTokens" class="form-input" placeholder="最大输出">
3018
+ </div>
3019
+ <div class="form-hint">留空表示不改动已有配置。</div>
3020
+ </div>
3021
+ </div>
3022
+
3023
+ <div class="quick-card">
3024
+ <div class="structured-card-title">选项</div>
3025
+ <label class="quick-option">
3026
+ <input type="checkbox" v-model="openclawQuick.setPrimary">
3027
+ 设为主模型
3028
+ </label>
3029
+ <label class="quick-option">
3030
+ <input type="checkbox" v-model="openclawQuick.overrideProvider">
3031
+ 覆盖同名 Provider 基础信息
3032
+ </label>
3033
+ <label class="quick-option">
3034
+ <input type="checkbox" v-model="openclawQuick.overrideModels">
3035
+ 覆盖同名模型列表
3036
+ </label>
3037
+ <div class="form-hint">关闭覆盖会只补空缺字段。</div>
3038
+ </div>
3039
+ </div>
3040
+ <div class="btn-group">
3041
+ <button class="btn btn-confirm" @click="applyOpenclawQuickToText">写入编辑器</button>
3042
+ </div>
3043
+ </div>
3044
+
3045
+ <div class="structured-section">
3046
+ <div class="structured-header">
3047
+ <span class="structured-title">结构化配置(高级)</span>
3048
+ <span class="form-hint">写入编辑器会重排 JSON,注释可能丢失。</span>
3049
+ </div>
3050
+ <div class="structured-grid">
3051
+ <div class="structured-card">
3052
+ <div class="structured-card-title">Agents Defaults</div>
3053
+ <div class="form-group">
3054
+ <label class="form-label">主模型</label>
3055
+ <input v-model="openclawStructured.agentPrimary" class="form-input" placeholder="例如: provider/model">
3056
+ </div>
3057
+ <div class="form-group">
3058
+ <label class="form-label">备选模型</label>
3059
+ <div class="list-row" v-for="(item, index) in openclawStructured.agentFallbacks" :key="'fallback-' + index">
3060
+ <input v-model="openclawStructured.agentFallbacks[index]" class="form-input" placeholder="例如: provider/model">
3061
+ <button class="btn-mini delete" @click="removeOpenclawFallback(index)">删除</button>
3062
+ </div>
3063
+ <button class="btn-mini" @click="addOpenclawFallback">添加备选</button>
3064
+ </div>
3065
+ <div class="form-group">
3066
+ <label class="form-label">Workspace</label>
3067
+ <input v-model="openclawStructured.workspace" class="form-input" placeholder="例如: ~/.openclaw/workspace">
3068
+ </div>
3069
+ <div class="form-group">
3070
+ <label class="form-label">Timeout(s)</label>
3071
+ <input v-model="openclawStructured.timeout" class="form-input" placeholder="例如: 600">
3072
+ </div>
3073
+ <div class="form-group">
3074
+ <label class="form-label">Context Tokens</label>
3075
+ <input v-model="openclawStructured.contextTokens" class="form-input" placeholder="例如: 4096">
3076
+ </div>
3077
+ <div class="form-group">
3078
+ <label class="form-label">Max Concurrent</label>
3079
+ <input v-model="openclawStructured.maxConcurrent" class="form-input" placeholder="例如: 2">
3080
+ </div>
3081
+ </div>
3082
+
3083
+ <div class="structured-card">
3084
+ <div class="structured-card-title">Env</div>
3085
+ <div class="form-group">
3086
+ <label class="form-label">环境变量</label>
3087
+ <div class="list-row" v-for="(item, index) in openclawStructured.envItems" :key="'env-' + index">
3088
+ <input v-model="item.key" class="form-input" placeholder="KEY">
3089
+ <input v-model="item.value" class="form-input" :type="item.show ? 'text' : 'password'" placeholder="VALUE">
3090
+ <button class="btn-mini" @click="toggleOpenclawEnvItem(index)">
3091
+ {{ item.show ? '隐藏' : '显示' }}
3092
+ </button>
3093
+ <button class="btn-mini delete" @click="removeOpenclawEnvItem(index)">删除</button>
3094
+ </div>
3095
+ <button class="btn-mini" @click="addOpenclawEnvItem">添加变量</button>
3096
+ </div>
3097
+ </div>
3098
+
3099
+ <div class="structured-card">
3100
+ <div class="structured-card-title">Tools</div>
3101
+ <div class="form-group">
3102
+ <label class="form-label">Profile</label>
3103
+ <select v-model="openclawStructured.toolsProfile" class="form-input">
3104
+ <option value="default">default</option>
3105
+ <option value="strict">strict</option>
3106
+ <option value="permissive">permissive</option>
3107
+ <option value="custom">custom</option>
3108
+ </select>
3109
+ </div>
3110
+ <div class="form-group">
3111
+ <label class="form-label">Allow</label>
3112
+ <div class="list-row" v-for="(item, index) in openclawStructured.toolsAllow" :key="'allow-' + index">
3113
+ <input v-model="openclawStructured.toolsAllow[index]" class="form-input" placeholder="例如: fs.read*">
3114
+ <button class="btn-mini delete" @click="removeOpenclawToolsAllow(index)">删除</button>
3115
+ </div>
3116
+ <button class="btn-mini" @click="addOpenclawToolsAllow">添加 allow</button>
3117
+ </div>
3118
+ <div class="form-group">
3119
+ <label class="form-label">Deny</label>
3120
+ <div class="list-row" v-for="(item, index) in openclawStructured.toolsDeny" :key="'deny-' + index">
3121
+ <input v-model="openclawStructured.toolsDeny[index]" class="form-input" placeholder="例如: net.*">
3122
+ <button class="btn-mini delete" @click="removeOpenclawToolsDeny(index)">删除</button>
3123
+ </div>
3124
+ <button class="btn-mini" @click="addOpenclawToolsDeny">添加 deny</button>
3125
+ </div>
3126
+ </div>
3127
+
3128
+ <div class="structured-card">
3129
+ <div class="structured-card-title">Providers(只读)</div>
3130
+ <div v-if="openclawProviders.length === 0" class="form-hint">
3131
+ 未发现 providers 配置(可能使用内置 provider 或 auth profiles)。
3132
+ </div>
3133
+ <div v-else class="provider-list">
3134
+ <div class="provider-item" v-for="(provider, index) in openclawProviders" :key="provider.key + '-' + provider.source + '-' + index">
3135
+ <div class="provider-header">
3136
+ <span class="provider-name">{{ provider.key }}</span>
3137
+ <span class="provider-source">来源: {{ provider.source }}</span>
3138
+ <span v-if="provider.isActive" class="pill configured">使用中</span>
3139
+ </div>
3140
+ <div v-if="provider.fields.length === 0" class="form-hint">未配置字段</div>
3141
+ <div v-else class="provider-fields">
3142
+ <div class="provider-field" v-for="field in provider.fields" :key="provider.key + '-' + field.key">
3143
+ <span class="provider-field-key">{{ field.key }}</span>
3144
+ <span class="provider-field-value">{{ field.value }}</span>
3145
+ </div>
3146
+ </div>
3147
+ </div>
3148
+ </div>
3149
+ <div v-if="openclawMissingProviders.length" class="form-hint">
3150
+ 使用中的 provider 未在配置中显示:{{ openclawMissingProviders.join(', ') }}。
3151
+ </div>
3152
+ </div>
3153
+
3154
+ <div class="structured-card">
3155
+ <div class="structured-card-title">Agents(只读)</div>
3156
+ <div v-if="openclawAgentsList.length === 0" class="form-hint">
3157
+ 未发现 agents.list 配置。
3158
+ </div>
3159
+ <div v-else class="agent-list">
3160
+ <div class="agent-item" v-for="(agent, index) in openclawAgentsList" :key="agent.key + '-' + index">
3161
+ <div class="agent-header">
3162
+ <span class="agent-name">{{ agent.name }}</span>
3163
+ <span class="agent-id">ID: {{ agent.id }}</span>
3164
+ </div>
3165
+ <div class="agent-meta" v-if="agent.theme || agent.emoji || agent.avatar">
3166
+ <span v-if="agent.theme">主题: {{ agent.theme }}</span>
3167
+ <span v-if="agent.emoji">表情: {{ agent.emoji }}</span>
3168
+ <span v-if="agent.avatar">头像: {{ agent.avatar }}</span>
3169
+ </div>
3170
+ </div>
3171
+ </div>
3172
+ </div>
3173
+ </div>
3174
+ <div class="btn-group">
3175
+ <button class="btn btn-confirm secondary" @click="syncOpenclawStructuredFromText">从文本刷新</button>
3176
+ <button class="btn btn-confirm" @click="applyOpenclawStructuredToText">写入编辑器</button>
3177
+ </div>
3178
+ </div>
3179
+
2204
3180
  <div class="form-group">
2205
- <label class="form-label">模型</label>
2206
- <input v-model="editingConfig.model" class="form-input" placeholder="例如: claude-sonnet-4-20250514">
3181
+ <label class="form-label">OpenClaw 配置(JSON5)</label>
3182
+ <textarea
3183
+ v-model="openclawEditing.content"
3184
+ class="form-input template-editor"
3185
+ spellcheck="false"
3186
+ placeholder="在这里编辑 OpenClaw 配置(JSON5)"></textarea>
3187
+ <div class="template-editor-warning">
3188
+ 保存仅写入本地配置库。点击“保存并应用”后会写入 openclaw.json。
3189
+ </div>
2207
3190
  </div>
2208
3191
 
2209
3192
  <div class="btn-group">
2210
- <button class="btn btn-cancel" @click="closeEditConfigModal">取消</button>
2211
- <button class="btn btn-confirm" @click="updateConfig">保存</button>
2212
- <button class="btn btn-confirm secondary" @click="saveAndApplyConfig">保存并应用到 Claude 配置</button>
2213
- <button class="btn btn-confirm secondary" @click="saveAndApplyEnvCompat">兼容模式应用到环境变量</button>
2214
- </div>
2215
- </div>
2216
- </div>
2217
-
2218
- <div v-if="showOpenclawConfigModal" class="modal-overlay" @click.self="closeOpenclawConfigModal">
2219
- <div class="modal modal-wide">
2220
- <div class="modal-title">{{ openclawEditorTitle }}</div>
2221
-
2222
- <div class="form-group">
2223
- <label class="form-label">配置名称</label>
2224
- <input v-model="openclawEditing.name" class="form-input" :readonly="openclawEditing.lockName" placeholder="例如: 默认配置">
2225
- </div>
2226
-
2227
- <div class="form-group">
2228
- <label class="form-label">目标文件</label>
2229
- <div class="form-hint">
2230
- {{ openclawConfigPath || '未加载' }}
2231
- <span v-if="openclawConfigPath">
2232
- ({{ openclawConfigExists ? '已存在' : '不存在,将在应用时创建' }})
2233
- </span>
2234
- </div>
2235
- <div class="btn-group" style="justify-content:flex-start;">
2236
- <button class="btn btn-confirm secondary" @click="loadOpenclawConfigFromFile" :disabled="openclawFileLoading">
2237
- {{ openclawFileLoading ? '加载中...' : '加载当前配置' }}
2238
- </button>
2239
- </div>
2240
- </div>
2241
-
2242
- <div class="form-group">
2243
- <label class="form-label">OpenClaw 配置(JSON5)</label>
2244
- <textarea
2245
- v-model="openclawEditing.content"
2246
- class="form-input template-editor"
2247
- spellcheck="false"
2248
- placeholder="在这里编辑 OpenClaw 配置(JSON5)"></textarea>
2249
- <div class="template-editor-warning">
2250
- 保存仅写入本地配置库;点击“保存并应用”后会写入 openclaw.json。
2251
- </div>
2252
- </div>
2253
-
2254
- <div class="btn-group">
2255
- <button class="btn btn-cancel" @click="closeOpenclawConfigModal">取消</button>
2256
- <button class="btn btn-confirm" @click="saveOpenclawConfig" :disabled="openclawSaving">
2257
- {{ openclawSaving ? '保存中...' : '保存' }}
2258
- </button>
2259
- <button class="btn btn-confirm secondary" @click="saveAndApplyOpenclawConfig" :disabled="openclawApplying">
2260
- {{ openclawApplying ? '应用中...' : '保存并应用' }}
2261
- </button>
2262
- </div>
2263
- </div>
2264
- </div>
2265
-
2266
- <div v-if="showConfigTemplateModal" class="modal-overlay" @click.self="closeConfigTemplateModal">
2267
- <div class="modal modal-wide">
2268
- <div class="modal-title">Config 模板编辑器(手动确认应用)</div>
3193
+ <button class="btn btn-cancel" @click="closeOpenclawConfigModal">取消</button>
3194
+ <button class="btn btn-confirm" @click="saveOpenclawConfig" :disabled="openclawSaving">
3195
+ {{ openclawSaving ? '保存中...' : '保存' }}
3196
+ </button>
3197
+ <button class="btn btn-confirm secondary" @click="saveAndApplyOpenclawConfig" :disabled="openclawApplying">
3198
+ {{ openclawApplying ? '应用中...' : '保存并应用' }}
3199
+ </button>
3200
+ </div>
3201
+ </div>
3202
+ </div>
3203
+
3204
+ <div v-if="showConfigTemplateModal" class="modal-overlay" @click.self="closeConfigTemplateModal">
3205
+ <div class="modal modal-wide">
3206
+ <div class="modal-title">Config 模板编辑器(手动确认应用)</div>
2269
3207
 
2270
3208
  <div class="form-group">
2271
3209
  <label class="form-label">config.toml 模板</label>
@@ -2275,7 +3213,7 @@
2275
3213
  spellcheck="false"
2276
3214
  placeholder="在这里编辑 config.toml 模板内容"></textarea>
2277
3215
  <div class="template-editor-warning">
2278
- 工具不会自动改动 `config.toml`;只有点击“确认应用模板”后才写入。
3216
+ 工具不会自动改动 `config.toml`。只有点击“确认应用模板”后才写入。
2279
3217
  </div>
2280
3218
  </div>
2281
3219
 
@@ -2288,9 +3226,17 @@
2288
3226
  </div>
2289
3227
  </div>
2290
3228
 
2291
- <div v-if="showAgentsModal" class="modal-overlay" @click.self="closeAgentsModal">
2292
- <div class="modal modal-wide">
2293
- <div class="modal-title">{{ agentsModalTitle }}</div>
3229
+ <div v-if="showAgentsModal" class="modal-overlay" @click.self="closeAgentsModal">
3230
+ <div class="modal modal-wide">
3231
+ <div class="modal-header">
3232
+ <div class="modal-title">{{ agentsModalTitle }}</div>
3233
+ <button
3234
+ class="btn-mini btn-modal-copy"
3235
+ @click="copyAgentsContent"
3236
+ :disabled="agentsLoading">
3237
+ 复制
3238
+ </button>
3239
+ </div>
2294
3240
 
2295
3241
  <div class="form-group">
2296
3242
  <label class="form-label">目标文件</label>
@@ -2304,16 +3250,16 @@
2304
3250
 
2305
3251
  <div class="form-group">
2306
3252
  <label class="form-label">AGENTS.md 内容</label>
2307
- <textarea
2308
- v-model="agentsContent"
2309
- class="form-input template-editor"
2310
- spellcheck="false"
2311
- :readonly="agentsLoading"
2312
- placeholder="在这里编辑 AGENTS.md 内容"></textarea>
2313
- <div class="template-editor-warning">
2314
- {{ agentsModalHint }}
2315
- </div>
2316
- </div>
3253
+ <textarea
3254
+ v-model="agentsContent"
3255
+ class="form-input template-editor"
3256
+ spellcheck="false"
3257
+ :readonly="agentsLoading"
3258
+ placeholder="在这里编辑 AGENTS.md 内容"></textarea>
3259
+ <div class="template-editor-warning">
3260
+ {{ agentsModalHint }}
3261
+ </div>
3262
+ </div>
2317
3263
 
2318
3264
  <div class="btn-group">
2319
3265
  <button class="btn btn-cancel" @click="closeAgentsModal">取消</button>
@@ -2330,22 +3276,22 @@
2330
3276
  <div v-if="message" :class="['toast', messageType]">{{ message }}</div>
2331
3277
  </div>
2332
3278
 
2333
- <script>
2334
- const { createApp } = Vue;
2335
- const API_BASE = 'http://localhost:3737';
2336
- const DEFAULT_OPENCLAW_TEMPLATE = `{
2337
- // OpenClaw config (JSON5)
2338
- agent: {
2339
- model: "gpt-4.1"
2340
- },
2341
- agents: {
2342
- defaults: {
2343
- workspace: "~/.openclaw/workspace"
2344
- }
2345
- }
2346
- }`;
2347
-
2348
- async function api(action, params = {}) {
3279
+ <script>
3280
+ const { createApp } = Vue;
3281
+ const API_BASE = 'http://localhost:3737';
3282
+ const DEFAULT_OPENCLAW_TEMPLATE = `{
3283
+ // OpenClaw config (JSON5)
3284
+ agent: {
3285
+ model: "gpt-4.1"
3286
+ },
3287
+ agents: {
3288
+ defaults: {
3289
+ workspace: "~/.openclaw/workspace"
3290
+ }
3291
+ }
3292
+ }`;
3293
+
3294
+ async function api(action, params = {}) {
2349
3295
  const res = await fetch(`${API_BASE}/api`, {
2350
3296
  method: 'POST',
2351
3297
  headers: { 'Content-Type': 'application/json' },
@@ -2362,6 +3308,13 @@
2362
3308
  currentModel: '',
2363
3309
  providersList: [],
2364
3310
  models: [],
3311
+ codexModelsLoading: false,
3312
+ modelsSource: 'remote',
3313
+ modelsHasCurrent: true,
3314
+ claudeModels: [],
3315
+ claudeModelsSource: 'idle',
3316
+ claudeModelsHasCurrent: true,
3317
+ claudeModelsLoading: false,
2365
3318
  loading: true,
2366
3319
  initError: '',
2367
3320
  message: '',
@@ -2370,23 +3323,23 @@
2370
3323
  showEditModal: false,
2371
3324
  showModelModal: false,
2372
3325
  showModelListModal: false,
2373
- showClaudeConfigModal: false,
2374
- showEditConfigModal: false,
2375
- showOpenclawConfigModal: false,
2376
- showConfigTemplateModal: false,
2377
- showAgentsModal: false,
2378
- configTemplateContent: '',
2379
- configTemplateApplying: false,
2380
- agentsContent: '',
2381
- agentsPath: '',
2382
- agentsExists: false,
2383
- agentsLineEnding: '\n',
2384
- agentsLoading: false,
2385
- agentsSaving: false,
2386
- agentsContext: 'codex',
2387
- agentsModalTitle: 'AGENTS.md 编辑器',
2388
- agentsModalHint: '保存后会写入目标 AGENTS.md(与 config.toml 同级)。',
2389
- sessionsList: [],
3326
+ showClaudeConfigModal: false,
3327
+ showEditConfigModal: false,
3328
+ showOpenclawConfigModal: false,
3329
+ showConfigTemplateModal: false,
3330
+ showAgentsModal: false,
3331
+ configTemplateContent: '',
3332
+ configTemplateApplying: false,
3333
+ agentsContent: '',
3334
+ agentsPath: '',
3335
+ agentsExists: false,
3336
+ agentsLineEnding: '\n',
3337
+ agentsLoading: false,
3338
+ agentsSaving: false,
3339
+ agentsContext: 'codex',
3340
+ agentsModalTitle: 'AGENTS.md 编辑器',
3341
+ agentsModalHint: '保存后会写入目标 AGENTS.md(与 config.toml 同级)。',
3342
+ sessionsList: [],
2390
3343
  sessionsLoading: false,
2391
3344
  sessionFilterSource: 'all',
2392
3345
  sessionPathFilter: '',
@@ -2407,20 +3360,30 @@
2407
3360
  },
2408
3361
  sessionPathRequestSeq: 0,
2409
3362
  sessionExporting: {},
3363
+ sessionCloning: {},
3364
+ sessionDeleting: {},
2410
3365
  activeSession: null,
2411
3366
  activeSessionMessages: [],
2412
3367
  activeSessionDetailError: '',
2413
3368
  activeSessionDetailClipped: false,
2414
- sessionDetailLoading: false,
2415
- sessionDetailRequestSeq: 0,
2416
- speedResults: {},
3369
+ sessionDetailLoading: false,
3370
+ sessionDetailRequestSeq: 0,
3371
+ sessionStandalone: false,
3372
+ sessionStandaloneError: '',
3373
+ sessionStandaloneText: '',
3374
+ sessionStandaloneTitle: '',
3375
+ sessionStandaloneSourceLabel: '',
3376
+ sessionStandaloneLoading: false,
3377
+ sessionStandaloneRequestSeq: 0,
3378
+ speedResults: {},
2417
3379
  speedLoading: {},
2418
3380
  newProvider: { name: '', url: '', key: '' },
2419
3381
  editingProvider: { name: '', url: '', key: '' },
2420
- newModelName: '',
2421
- currentClaudeConfig: '',
2422
- editingConfig: { name: '', apiKey: '', baseUrl: '', model: '' },
2423
- claudeConfigs: {
3382
+ newModelName: '',
3383
+ currentClaudeConfig: '',
3384
+ currentClaudeModel: '',
3385
+ editingConfig: { name: '', apiKey: '', baseUrl: '', model: '' },
3386
+ claudeConfigs: {
2424
3387
  '智谱GLM': {
2425
3388
  apiKey: '',
2426
3389
  baseUrl: 'https://open.bigmodel.cn/api/anthropic',
@@ -2428,33 +3391,70 @@
2428
3391
  hasKey: false
2429
3392
  }
2430
3393
  },
2431
- newClaudeConfig: {
2432
- name: '',
2433
- apiKey: '',
2434
- baseUrl: 'https://open.bigmodel.cn/api/anthropic',
2435
- model: 'glm-4.7'
2436
- },
2437
- currentOpenclawConfig: '',
2438
- openclawConfigs: {
2439
- '默认配置': {
2440
- content: DEFAULT_OPENCLAW_TEMPLATE
2441
- }
2442
- },
2443
- openclawEditing: { name: '', content: '', lockName: false },
2444
- openclawEditorTitle: '添加 OpenClaw 配置',
2445
- openclawConfigPath: '',
2446
- openclawConfigExists: false,
2447
- openclawLineEnding: '\n',
2448
- openclawFileLoading: false,
2449
- openclawSaving: false,
2450
- openclawApplying: false
2451
- }
2452
- },
2453
- mounted() {
2454
- const savedConfigs = localStorage.getItem('claudeConfigs');
2455
- if (savedConfigs) {
2456
- try {
2457
- this.claudeConfigs = JSON.parse(savedConfigs);
3394
+ newClaudeConfig: {
3395
+ name: '',
3396
+ apiKey: '',
3397
+ baseUrl: 'https://open.bigmodel.cn/api/anthropic',
3398
+ model: 'glm-4.7'
3399
+ },
3400
+ currentOpenclawConfig: '',
3401
+ openclawConfigs: {
3402
+ '默认配置': {
3403
+ content: DEFAULT_OPENCLAW_TEMPLATE
3404
+ }
3405
+ },
3406
+ openclawEditing: { name: '', content: '', lockName: false },
3407
+ openclawEditorTitle: '添加 OpenClaw 配置',
3408
+ openclawConfigPath: '',
3409
+ openclawConfigExists: false,
3410
+ openclawLineEnding: '\n',
3411
+ openclawFileLoading: false,
3412
+ openclawSaving: false,
3413
+ openclawApplying: false,
3414
+ openclawWorkspaceFileName: 'SOUL.md',
3415
+ agentsWorkspaceFileName: '',
3416
+ openclawStructured: {
3417
+ agentPrimary: '',
3418
+ agentFallbacks: [],
3419
+ workspace: '',
3420
+ timeout: '',
3421
+ contextTokens: '',
3422
+ maxConcurrent: '',
3423
+ envItems: [],
3424
+ toolsProfile: 'default',
3425
+ toolsAllow: [],
3426
+ toolsDeny: []
3427
+ },
3428
+ openclawQuick: {
3429
+ providerName: '',
3430
+ baseUrl: '',
3431
+ apiKey: '',
3432
+ apiType: 'openai-responses',
3433
+ modelId: '',
3434
+ modelName: '',
3435
+ contextWindow: '',
3436
+ maxTokens: '',
3437
+ setPrimary: true,
3438
+ overrideProvider: true,
3439
+ overrideModels: true,
3440
+ showKey: false
3441
+ },
3442
+ openclawAgentsList: [],
3443
+ openclawProviders: [],
3444
+ openclawMissingProviders: [],
3445
+ recentConfigs: [],
3446
+ recentLoading: false,
3447
+ healthCheckLoading: false,
3448
+ healthCheckResult: null,
3449
+ healthCheckRemote: false
3450
+ }
3451
+ },
3452
+ mounted() {
3453
+ this.initSessionStandalone();
3454
+ const savedConfigs = localStorage.getItem('claudeConfigs');
3455
+ if (savedConfigs) {
3456
+ try {
3457
+ this.claudeConfigs = JSON.parse(savedConfigs);
2458
3458
  for (const [name, config] of Object.entries(this.claudeConfigs)) {
2459
3459
  if (config.apiKey && config.apiKey.includes('****')) {
2460
3460
  config.apiKey = '';
@@ -2469,37 +3469,50 @@
2469
3469
  }
2470
3470
  } catch (e) {
2471
3471
  console.error('加载 Claude 配置失败:', e);
2472
- }
2473
- }
2474
- const savedOpenclawConfigs = localStorage.getItem('openclawConfigs');
2475
- if (savedOpenclawConfigs) {
2476
- try {
2477
- this.openclawConfigs = JSON.parse(savedOpenclawConfigs);
2478
- const configNames = Object.keys(this.openclawConfigs);
2479
- if (configNames.length > 0) {
2480
- this.currentOpenclawConfig = configNames[0];
2481
- }
2482
- } catch (e) {
2483
- console.error('加载 OpenClaw 配置失败:', e);
2484
- }
2485
- } else {
2486
- const configNames = Object.keys(this.openclawConfigs);
2487
- if (configNames.length > 0) {
2488
- this.currentOpenclawConfig = configNames[0];
2489
- }
2490
- }
2491
- this.loadAll();
2492
- },
3472
+ }
3473
+ }
3474
+ if (!this.currentClaudeConfig) {
3475
+ const configNames = Object.keys(this.claudeConfigs);
3476
+ if (configNames.length > 0) {
3477
+ this.currentClaudeConfig = configNames[0];
3478
+ }
3479
+ }
3480
+ this.syncClaudeModelFromConfig();
3481
+ const savedOpenclawConfigs = localStorage.getItem('openclawConfigs');
3482
+ if (savedOpenclawConfigs) {
3483
+ try {
3484
+ this.openclawConfigs = JSON.parse(savedOpenclawConfigs);
3485
+ const configNames = Object.keys(this.openclawConfigs);
3486
+ if (configNames.length > 0) {
3487
+ this.currentOpenclawConfig = configNames[0];
3488
+ }
3489
+ } catch (e) {
3490
+ console.error('加载 OpenClaw 配置失败:', e);
3491
+ }
3492
+ } else {
3493
+ const configNames = Object.keys(this.openclawConfigs);
3494
+ if (configNames.length > 0) {
3495
+ this.currentOpenclawConfig = configNames[0];
3496
+ }
3497
+ }
3498
+ this.loadAll();
3499
+ },
3500
+
3501
+ computed: {
3502
+ isSessionQueryEnabled() {
3503
+ return this.sessionFilterSource === 'codex';
3504
+ },
3505
+ sessionQueryPlaceholder() {
3506
+ return this.isSessionQueryEnabled ? '关键词检索' : '仅 Codex 支持关键词检索';
3507
+ }
3508
+ },
2493
3509
  methods: {
2494
3510
  async loadAll() {
2495
3511
  this.loading = true;
2496
3512
  this.initError = '';
2497
3513
  try {
2498
- const [statusRes, listRes, modelsRes] = await Promise.all([
2499
- api('status'),
2500
- api('list'),
2501
- api('models')
2502
- ]);
3514
+ const statusRes = await api('status');
3515
+ const listRes = await api('list');
2503
3516
 
2504
3517
  if (statusRes.error) {
2505
3518
  this.initError = statusRes.error;
@@ -2507,14 +3520,16 @@
2507
3520
  this.currentProvider = statusRes.provider;
2508
3521
  this.currentModel = statusRes.model;
2509
3522
  this.providersList = listRes.providers;
2510
- this.models = modelsRes.models;
3523
+ await this.loadModelsForProvider(this.currentProvider);
2511
3524
  if (statusRes.configReady === false) {
2512
- this.showMessage(statusRes.configNotice || '未检测到 config.toml,已加载默认模板;请在模板编辑器确认后创建。', 'info');
3525
+ this.showMessage(statusRes.configNotice || '未检测到 config.toml,已加载默认模板。请在模板编辑器确认后创建。', 'info');
2513
3526
  }
2514
3527
  if (statusRes.initNotice) {
2515
3528
  this.showMessage(statusRes.initNotice, 'info');
2516
3529
  }
3530
+ this.maybeShowStarPrompt();
2517
3531
  }
3532
+ await this.loadRecentConfigs();
2518
3533
  } catch (e) {
2519
3534
  this.initError = '连接失败: ' + e.message;
2520
3535
  } finally {
@@ -2522,15 +3537,230 @@
2522
3537
  }
2523
3538
  },
2524
3539
 
2525
- switchConfigMode(mode) {
2526
- this.configMode = mode;
2527
- if (mode === 'sessions' && this.sessionsList.length === 0) {
2528
- this.loadSessions();
3540
+ async loadModelsForProvider(providerName) {
3541
+ this.codexModelsLoading = true;
3542
+ if (!providerName) {
3543
+ this.models = [];
3544
+ this.modelsSource = 'unlimited';
3545
+ this.modelsHasCurrent = true;
3546
+ this.codexModelsLoading = false;
3547
+ return;
3548
+ }
3549
+ try {
3550
+ const res = await api('models', { provider: providerName });
3551
+ if (res.unlimited) {
3552
+ this.models = [];
3553
+ this.modelsSource = 'unlimited';
3554
+ this.modelsHasCurrent = true;
3555
+ return;
3556
+ }
3557
+ if (res.error) {
3558
+ this.showMessage('模型列表获取失败: ' + res.error, 'error');
3559
+ this.models = [];
3560
+ this.modelsSource = 'error';
3561
+ this.modelsHasCurrent = true;
3562
+ return;
3563
+ }
3564
+ const list = Array.isArray(res.models) ? res.models : [];
3565
+ this.models = list;
3566
+ this.modelsSource = res.source || 'remote';
3567
+ this.modelsHasCurrent = !!this.currentModel && list.includes(this.currentModel);
3568
+ } catch (e) {
3569
+ this.showMessage('模型列表获取失败: ' + e.message, 'error');
3570
+ this.models = [];
3571
+ this.modelsSource = 'error';
3572
+ this.modelsHasCurrent = true;
3573
+ } finally {
3574
+ this.codexModelsLoading = false;
2529
3575
  }
2530
3576
  },
2531
3577
 
2532
- getSessionExportKey(session) {
2533
- return `${session.source || 'unknown'}:${session.sessionId || ''}:${session.filePath || ''}`;
3578
+ getCurrentClaudeConfig() {
3579
+ if (!this.currentClaudeConfig) return null;
3580
+ return this.claudeConfigs[this.currentClaudeConfig] || null;
3581
+ },
3582
+
3583
+ syncClaudeModelFromConfig() {
3584
+ const config = this.getCurrentClaudeConfig();
3585
+ this.currentClaudeModel = config && config.model ? config.model : '';
3586
+ },
3587
+
3588
+ refreshClaudeModelContext() {
3589
+ this.syncClaudeModelFromConfig();
3590
+ this.loadClaudeModels();
3591
+ },
3592
+
3593
+ resetClaudeModelsState() {
3594
+ this.claudeModels = [];
3595
+ this.claudeModelsSource = 'idle';
3596
+ this.claudeModelsHasCurrent = true;
3597
+ this.claudeModelsLoading = false;
3598
+ },
3599
+
3600
+ updateClaudeModelsCurrent() {
3601
+ const currentModel = (this.currentClaudeModel || '').trim();
3602
+ this.claudeModelsHasCurrent = !!currentModel && this.claudeModels.includes(currentModel);
3603
+ },
3604
+
3605
+ async loadClaudeModels() {
3606
+ const config = this.getCurrentClaudeConfig();
3607
+ const baseUrl = (config.baseUrl || '').trim();
3608
+ const apiKey = (config.apiKey || '').trim();
3609
+
3610
+ if (!baseUrl) {
3611
+ this.resetClaudeModelsState();
3612
+ return;
3613
+ }
3614
+
3615
+ this.claudeModelsLoading = true;
3616
+ try {
3617
+ const res = await api('models-by-url', { baseUrl, apiKey });
3618
+ if (res.unlimited) {
3619
+ this.claudeModels = [];
3620
+ this.claudeModelsSource = 'unlimited';
3621
+ this.claudeModelsHasCurrent = true;
3622
+ return;
3623
+ }
3624
+ if (res.error) {
3625
+ this.showMessage('模型列表获取失败: ' + res.error, 'error');
3626
+ this.claudeModels = [];
3627
+ this.claudeModelsSource = 'error';
3628
+ this.claudeModelsHasCurrent = true;
3629
+ return;
3630
+ }
3631
+ const list = Array.isArray(res.models) ? res.models : [];
3632
+ this.claudeModels = list;
3633
+ this.claudeModelsSource = res.source || 'remote';
3634
+ this.updateClaudeModelsCurrent();
3635
+ } catch (e) {
3636
+ this.showMessage('模型列表获取失败: ' + e.message, 'error');
3637
+ this.claudeModels = [];
3638
+ this.claudeModelsSource = 'error';
3639
+ this.claudeModelsHasCurrent = true;
3640
+ } finally {
3641
+ this.claudeModelsLoading = false;
3642
+ }
3643
+ },
3644
+
3645
+ openClaudeConfigModal() {
3646
+ this.showClaudeConfigModal = true;
3647
+ },
3648
+
3649
+ maybeShowStarPrompt() {
3650
+ const storageKey = 'codexmateStarPrompted';
3651
+ if (localStorage.getItem(storageKey)) {
3652
+ return;
3653
+ }
3654
+ this.showMessage('如果 Codex Mate 对你有帮助,欢迎到 GitHub 点个 Star。', 'info');
3655
+ localStorage.setItem(storageKey, '1');
3656
+ },
3657
+
3658
+ switchConfigMode(mode) {
3659
+ this.configMode = mode;
3660
+ if (mode === 'claude') {
3661
+ this.refreshClaudeModelContext();
3662
+ }
3663
+ if (mode === 'sessions' && this.sessionsList.length === 0) {
3664
+ this.loadSessions();
3665
+ }
3666
+ },
3667
+
3668
+ getSessionStandaloneContext() {
3669
+ try {
3670
+ const url = new URL(window.location.href);
3671
+ if (url.pathname !== '/session') {
3672
+ return { requested: false, params: null, error: '' };
3673
+ }
3674
+
3675
+ const source = (url.searchParams.get('source') || '').trim().toLowerCase();
3676
+ const sessionId = (url.searchParams.get('sessionId') || url.searchParams.get('id') || '').trim();
3677
+ const filePath = (url.searchParams.get('filePath') || url.searchParams.get('path') || '').trim();
3678
+ let error = '';
3679
+ if (!source) {
3680
+ error = '缺少 source 参数';
3681
+ } else if (source !== 'codex' && source !== 'claude') {
3682
+ error = 'source 仅支持 codex 或 claude';
3683
+ }
3684
+ if (!sessionId && !filePath) {
3685
+ error = error ? `${error},还缺少 sessionId 或 filePath` : '缺少 sessionId 或 filePath 参数';
3686
+ }
3687
+
3688
+ if (error) {
3689
+ return { requested: true, params: null, error };
3690
+ }
3691
+
3692
+ return {
3693
+ requested: true,
3694
+ params: {
3695
+ source,
3696
+ sessionId,
3697
+ filePath
3698
+ },
3699
+ error: ''
3700
+ };
3701
+ } catch (_) {
3702
+ return { requested: false, params: null, error: '' };
3703
+ }
3704
+ },
3705
+
3706
+ initSessionStandalone() {
3707
+ const context = this.getSessionStandaloneContext();
3708
+ if (!context.requested) return;
3709
+
3710
+ this.sessionStandalone = true;
3711
+ this.configMode = 'sessions';
3712
+
3713
+ if (context.error || !context.params) {
3714
+ this.sessionStandaloneError = `会话链接参数不完整:${context.error || '参数解析失败'}`;
3715
+ return;
3716
+ }
3717
+
3718
+ const sourceLabel = context.params.source === 'codex' ? 'Codex' : 'Claude Code';
3719
+ this.activeSession = {
3720
+ source: context.params.source,
3721
+ sourceLabel,
3722
+ sessionId: context.params.sessionId,
3723
+ filePath: context.params.filePath,
3724
+ title: context.params.sessionId || context.params.filePath || '会话'
3725
+ };
3726
+ this.activeSessionMessages = [];
3727
+ this.activeSessionDetailError = '';
3728
+ this.activeSessionDetailClipped = false;
3729
+ this.sessionStandaloneError = '';
3730
+ this.sessionStandaloneText = '';
3731
+ this.sessionStandaloneTitle = this.activeSession.title || '会话';
3732
+ this.sessionStandaloneSourceLabel = sourceLabel;
3733
+ this.loadSessionStandalonePlain();
3734
+ },
3735
+
3736
+ buildSessionStandaloneUrl(session) {
3737
+ if (!session) return '';
3738
+ const source = typeof session.source === 'string' ? session.source.trim().toLowerCase() : '';
3739
+ if (!source || (source !== 'codex' && source !== 'claude')) return '';
3740
+ const sessionId = typeof session.sessionId === 'string' ? session.sessionId.trim() : '';
3741
+ const filePath = typeof session.filePath === 'string' ? session.filePath.trim() : '';
3742
+ if (!sessionId && !filePath) return '';
3743
+ const origin = window.location.origin && window.location.origin !== 'null'
3744
+ ? window.location.origin
3745
+ : API_BASE;
3746
+ const params = new URLSearchParams();
3747
+ params.set('source', source);
3748
+ if (sessionId) params.set('sessionId', sessionId);
3749
+ if (filePath) params.set('filePath', filePath);
3750
+ return `${origin}/session?${params.toString()}`;
3751
+ },
3752
+
3753
+ openSessionStandalone(session) {
3754
+ const url = this.buildSessionStandaloneUrl(session);
3755
+ if (!url) {
3756
+ this.showMessage('当前会话无法生成新页链接', 'error');
3757
+ return;
3758
+ }
3759
+ window.open(url, '_blank', 'noopener');
3760
+ },
3761
+
3762
+ getSessionExportKey(session) {
3763
+ return `${session.source || 'unknown'}:${session.sessionId || ''}:${session.filePath || ''}`;
2534
3764
  },
2535
3765
 
2536
3766
  isResumeCommandAvailable(session) {
@@ -2540,6 +3770,23 @@
2540
3770
  return source === 'codex' && !!sessionId;
2541
3771
  },
2542
3772
 
3773
+ isCloneAvailable(session) {
3774
+ if (!session) return false;
3775
+ const source = String(session.source || '').trim().toLowerCase();
3776
+ const sessionId = typeof session.sessionId === 'string' ? session.sessionId.trim() : '';
3777
+ const filePath = typeof session.filePath === 'string' ? session.filePath.trim() : '';
3778
+ return source === 'codex' && (!!sessionId || !!filePath);
3779
+ },
3780
+
3781
+ isDeleteAvailable(session) {
3782
+ if (!session) return false;
3783
+ const source = String(session.source || '').trim().toLowerCase();
3784
+ if (source !== 'codex' && source !== 'claude') return false;
3785
+ const sessionId = typeof session.sessionId === 'string' ? session.sessionId.trim() : '';
3786
+ const filePath = typeof session.filePath === 'string' ? session.filePath.trim() : '';
3787
+ return !!sessionId || !!filePath;
3788
+ },
3789
+
2543
3790
  buildResumeCommand(session) {
2544
3791
  const sessionId = session && session.sessionId ? String(session.sessionId).trim() : '';
2545
3792
  return `codex resume ${this.quoteResumeArg(sessionId)}`;
@@ -2576,6 +3823,20 @@
2576
3823
  }
2577
3824
  },
2578
3825
 
3826
+ copyAgentsContent() {
3827
+ const text = typeof this.agentsContent === 'string' ? this.agentsContent : '';
3828
+ if (!text) {
3829
+ this.showMessage('没有可复制的内容', 'info');
3830
+ return;
3831
+ }
3832
+ const ok = this.fallbackCopyText(text);
3833
+ if (ok) {
3834
+ this.showMessage('已复制 AGENTS.md 内容', 'success');
3835
+ return;
3836
+ }
3837
+ this.showMessage('复制失败,请手动复制内容', 'error');
3838
+ },
3839
+
2579
3840
  async copyResumeCommand(session) {
2580
3841
  if (!this.isResumeCommandAvailable(session)) {
2581
3842
  this.showMessage('当前会话不支持生成恢复命令', 'error');
@@ -2599,6 +3860,71 @@
2599
3860
  this.showMessage('复制失败,请手动复制命令', 'error');
2600
3861
  },
2601
3862
 
3863
+ async cloneSession(session) {
3864
+ if (!this.isCloneAvailable(session)) {
3865
+ this.showMessage('当前会话不支持克隆', 'error');
3866
+ return;
3867
+ }
3868
+ const key = this.getSessionExportKey(session);
3869
+ if (this.sessionCloning[key]) {
3870
+ return;
3871
+ }
3872
+ this.sessionCloning[key] = true;
3873
+ try {
3874
+ const res = await api('clone-session', {
3875
+ source: session.source,
3876
+ sessionId: session.sessionId,
3877
+ filePath: session.filePath
3878
+ });
3879
+ if (res.error) {
3880
+ this.showMessage(res.error, 'error');
3881
+ return;
3882
+ }
3883
+
3884
+ this.showMessage('会话已克隆', 'success');
3885
+ await this.loadSessions();
3886
+ if (res.sessionId) {
3887
+ const matched = this.sessionsList.find(item => item.source === 'codex' && item.sessionId === res.sessionId);
3888
+ if (matched) {
3889
+ await this.selectSession(matched);
3890
+ }
3891
+ }
3892
+ } catch (e) {
3893
+ this.showMessage('克隆失败: ' + e.message, 'error');
3894
+ } finally {
3895
+ this.sessionCloning[key] = false;
3896
+ }
3897
+ },
3898
+
3899
+ async deleteSession(session) {
3900
+ if (!this.isDeleteAvailable(session)) {
3901
+ this.showMessage('当前会话不支持删除', 'error');
3902
+ return;
3903
+ }
3904
+ const key = this.getSessionExportKey(session);
3905
+ if (this.sessionDeleting[key]) {
3906
+ return;
3907
+ }
3908
+ this.sessionDeleting[key] = true;
3909
+ try {
3910
+ const res = await api('delete-session', {
3911
+ source: session.source,
3912
+ sessionId: session.sessionId,
3913
+ filePath: session.filePath
3914
+ });
3915
+ if (res.error) {
3916
+ this.showMessage(res.error, 'error');
3917
+ return;
3918
+ }
3919
+ this.showMessage('会话已删除', 'success');
3920
+ await this.loadSessions();
3921
+ } catch (e) {
3922
+ this.showMessage('删除失败: ' + e.message, 'error');
3923
+ } finally {
3924
+ this.sessionDeleting[key] = false;
3925
+ }
3926
+ },
3927
+
2602
3928
  normalizeSessionPathValue(value) {
2603
3929
  if (typeof value !== 'string') return '';
2604
3930
  return value.trim();
@@ -2762,7 +4088,7 @@
2762
4088
  if (this.sessionsLoading) return;
2763
4089
  this.sessionsLoading = true;
2764
4090
  this.activeSessionDetailError = '';
2765
- const query = this.sessionQuery;
4091
+ const query = this.isSessionQueryEnabled ? this.sessionQuery : '';
2766
4092
  try {
2767
4093
  const res = await api('list-sessions', {
2768
4094
  source: this.sessionFilterSource,
@@ -2770,7 +4096,7 @@
2770
4096
  query,
2771
4097
  queryMode: 'and',
2772
4098
  queryScope: 'content',
2773
- contentScanLimit: 2,
4099
+ contentScanLimit: 50,
2774
4100
  roleFilter: this.sessionRoleFilter,
2775
4101
  timeRangePreset: this.sessionTimePreset,
2776
4102
  limit: 200,
@@ -2812,21 +4138,66 @@
2812
4138
  }
2813
4139
  },
2814
4140
 
2815
- async selectSession(session) {
2816
- if (!session) return;
2817
- if (this.activeSession && this.getSessionExportKey(this.activeSession) === this.getSessionExportKey(session)) return;
2818
- this.activeSession = session;
2819
- this.activeSessionMessages = [];
2820
- this.activeSessionDetailError = '';
2821
- this.activeSessionDetailClipped = false;
2822
- await this.loadActiveSessionDetail();
2823
- },
2824
-
2825
- async loadActiveSessionDetail() {
2826
- if (!this.activeSession) {
2827
- this.activeSessionMessages = [];
2828
- this.activeSessionDetailError = '';
2829
- this.activeSessionDetailClipped = false;
4141
+ async selectSession(session) {
4142
+ if (!session) return;
4143
+ if (this.activeSession && this.getSessionExportKey(this.activeSession) === this.getSessionExportKey(session)) return;
4144
+ this.activeSession = session;
4145
+ this.activeSessionMessages = [];
4146
+ this.activeSessionDetailError = '';
4147
+ this.activeSessionDetailClipped = false;
4148
+ await this.loadActiveSessionDetail();
4149
+ },
4150
+
4151
+ async loadSessionStandalonePlain() {
4152
+ if (!this.activeSession) {
4153
+ this.sessionStandaloneText = '';
4154
+ this.sessionStandaloneTitle = '会话';
4155
+ this.sessionStandaloneSourceLabel = '';
4156
+ this.sessionStandaloneError = '';
4157
+ return;
4158
+ }
4159
+
4160
+ const requestSeq = ++this.sessionStandaloneRequestSeq;
4161
+ this.sessionStandaloneLoading = true;
4162
+ this.sessionStandaloneError = '';
4163
+ try {
4164
+ const res = await api('session-plain', {
4165
+ source: this.activeSession.source,
4166
+ sessionId: this.activeSession.sessionId,
4167
+ filePath: this.activeSession.filePath
4168
+ });
4169
+
4170
+ if (requestSeq !== this.sessionStandaloneRequestSeq) {
4171
+ return;
4172
+ }
4173
+
4174
+ if (res.error) {
4175
+ this.sessionStandaloneText = '';
4176
+ this.sessionStandaloneError = res.error;
4177
+ return;
4178
+ }
4179
+
4180
+ this.sessionStandaloneSourceLabel = res.sourceLabel || this.activeSession.sourceLabel || '';
4181
+ this.sessionStandaloneTitle = res.sessionId || this.activeSession.title || '会话';
4182
+ this.sessionStandaloneText = typeof res.text === 'string' ? res.text : '';
4183
+ } catch (e) {
4184
+ if (requestSeq !== this.sessionStandaloneRequestSeq) {
4185
+ return;
4186
+ }
4187
+ this.sessionStandaloneText = '';
4188
+ this.sessionStandaloneError = '加载会话内容失败: ' + e.message;
4189
+ } finally {
4190
+ if (requestSeq === this.sessionStandaloneRequestSeq) {
4191
+ this.sessionStandaloneLoading = false;
4192
+ }
4193
+ }
4194
+ },
4195
+
4196
+ async loadActiveSessionDetail() {
4197
+ if (!this.activeSession) {
4198
+ this.activeSessionMessages = [];
4199
+ this.activeSessionDetailError = '';
4200
+ this.activeSessionDetailClipped = false;
2830
4201
  return;
2831
4202
  }
2832
4203
 
@@ -2854,6 +4225,20 @@
2854
4225
 
2855
4226
  this.activeSessionMessages = Array.isArray(res.messages) ? res.messages : [];
2856
4227
  this.activeSessionDetailClipped = !!res.clipped;
4228
+ if (this.activeSession) {
4229
+ if (res.sourceLabel) {
4230
+ this.activeSession.sourceLabel = res.sourceLabel;
4231
+ }
4232
+ if (res.sessionId) {
4233
+ this.activeSession.sessionId = res.sessionId;
4234
+ if (!this.activeSession.title) {
4235
+ this.activeSession.title = res.sessionId;
4236
+ }
4237
+ }
4238
+ if (res.filePath) {
4239
+ this.activeSession.filePath = res.filePath;
4240
+ }
4241
+ }
2857
4242
  if (res.updatedAt) {
2858
4243
  this.activeSession.updatedAt = res.updatedAt;
2859
4244
  }
@@ -2898,23 +4283,29 @@
2898
4283
  sessionId: session.sessionId,
2899
4284
  filePath: session.filePath
2900
4285
  });
2901
- if (res.error) {
2902
- this.showMessage(res.error, 'error');
2903
- return;
2904
- }
2905
-
2906
- const fileName = res.fileName || `${session.source || 'session'}-${session.sessionId || Date.now()}.md`;
2907
- this.downloadTextFile(fileName, res.content || '');
2908
- this.showMessage('会话导出完成', 'success');
2909
- } catch (e) {
2910
- this.showMessage('导出失败: ' + e.message, 'error');
2911
- } finally {
4286
+ if (res.error) {
4287
+ this.showMessage(res.error, 'error');
4288
+ return;
4289
+ }
4290
+
4291
+ const fileName = res.fileName || `${session.source || 'session'}-${session.sessionId || Date.now()}.md`;
4292
+ this.downloadTextFile(fileName, res.content || '');
4293
+ if (res.truncated) {
4294
+ const maxLabel = res.maxMessages === 'all' ? 'all' : res.maxMessages;
4295
+ this.showMessage(`会话导出完成(已截断:最多 ${maxLabel} 条消息)`, 'info');
4296
+ } else {
4297
+ this.showMessage('会话导出完成', 'success');
4298
+ }
4299
+ } catch (e) {
4300
+ this.showMessage('导出失败: ' + e.message, 'error');
4301
+ } finally {
2912
4302
  this.sessionExporting[key] = false;
2913
4303
  }
2914
4304
  },
2915
4305
 
2916
4306
  async switchProvider(name) {
2917
4307
  this.currentProvider = name;
4308
+ await this.loadModelsForProvider(name);
2918
4309
  await this.openConfigTemplateEditor();
2919
4310
  },
2920
4311
 
@@ -2922,6 +4313,90 @@
2922
4313
  await this.openConfigTemplateEditor();
2923
4314
  },
2924
4315
 
4316
+ async loadRecentConfigs() {
4317
+ this.recentLoading = true;
4318
+ try {
4319
+ const res = await api('get-recent-configs');
4320
+ if (res && Array.isArray(res.items)) {
4321
+ this.recentConfigs = res.items;
4322
+ } else {
4323
+ this.recentConfigs = [];
4324
+ }
4325
+ } catch (e) {
4326
+ this.recentConfigs = [];
4327
+ } finally {
4328
+ this.recentLoading = false;
4329
+ }
4330
+ },
4331
+
4332
+ async applyRecentConfig(item) {
4333
+ if (!item || !item.provider || !item.model) {
4334
+ this.showMessage('最近配置无效,无法应用', 'error');
4335
+ return;
4336
+ }
4337
+ this.currentProvider = item.provider;
4338
+ this.currentModel = item.model;
4339
+ await this.openConfigTemplateEditor({
4340
+ appendHint: '最近使用配置,确认后将写入 config.toml'
4341
+ });
4342
+ },
4343
+
4344
+ async runHealthCheck() {
4345
+ this.healthCheckLoading = true;
4346
+ this.healthCheckResult = null;
4347
+ try {
4348
+ const res = await api('config-health-check', {
4349
+ remote: false
4350
+ });
4351
+ if (res && typeof res === 'object') {
4352
+ const issues = Array.isArray(res.issues) ? [...res.issues] : [];
4353
+ let remote = res.remote || null;
4354
+ {
4355
+ const providers = (this.providersList || [])
4356
+ .filter(provider => provider && provider.name);
4357
+ const tasks = providers.map(provider =>
4358
+ this.runSpeedTest(provider.name, { silent: true })
4359
+ .then(result => ({ name: provider.name, result }))
4360
+ .catch(err => ({
4361
+ name: provider.name,
4362
+ result: { ok: false, error: err && err.message ? err.message : 'Speed test failed' }
4363
+ }))
4364
+ );
4365
+ const pairs = await Promise.all(tasks);
4366
+ const results = {};
4367
+ for (const pair of pairs) {
4368
+ results[pair.name] = pair.result || null;
4369
+ const issue = this.buildSpeedTestIssue(pair.name, pair.result);
4370
+ if (issue) issues.push(issue);
4371
+ }
4372
+ remote = {
4373
+ type: 'speed-test',
4374
+ results
4375
+ };
4376
+ }
4377
+
4378
+ const ok = issues.length === 0;
4379
+ this.healthCheckResult = {
4380
+ ...res,
4381
+ ok,
4382
+ issues,
4383
+ remote
4384
+ };
4385
+ if (ok) {
4386
+ this.showMessage('健康检查通过', 'success');
4387
+ }
4388
+ } else {
4389
+ this.healthCheckResult = null;
4390
+ this.showMessage('健康检查失败:返回数据异常', 'error');
4391
+ }
4392
+ } catch (e) {
4393
+ this.healthCheckResult = null;
4394
+ this.showMessage('健康检查失败: ' + e.message, 'error');
4395
+ } finally {
4396
+ this.healthCheckLoading = false;
4397
+ }
4398
+ },
4399
+
2925
4400
  escapeTomlString(value) {
2926
4401
  return String(value || '')
2927
4402
  .replace(/\\/g, '\\\\')
@@ -2984,105 +4459,152 @@
2984
4459
  }
2985
4460
  },
2986
4461
 
2987
- async openAgentsEditor() {
2988
- this.setAgentsModalContext('codex');
2989
- this.agentsLoading = true;
2990
- try {
2991
- const res = await api('get-agents-file');
2992
- if (res.error) {
2993
- this.showMessage(res.error, 'error');
4462
+ async openAgentsEditor() {
4463
+ this.setAgentsModalContext('codex');
4464
+ this.agentsLoading = true;
4465
+ try {
4466
+ const res = await api('get-agents-file');
4467
+ if (res.error) {
4468
+ this.showMessage(res.error, 'error');
2994
4469
  return;
2995
4470
  }
2996
4471
  this.agentsContent = res.content || '';
2997
4472
  this.agentsPath = res.path || '';
2998
4473
  this.agentsExists = !!res.exists;
2999
- this.agentsLineEnding = res.lineEnding === '\r\n' ? '\r\n' : '\n';
3000
- this.showAgentsModal = true;
3001
- } catch (e) {
3002
- this.showMessage('加载 AGENTS.md 失败: ' + e.message, 'error');
3003
- } finally {
3004
- this.agentsLoading = false;
3005
- }
3006
- },
3007
-
3008
- async openOpenclawAgentsEditor() {
3009
- this.setAgentsModalContext('openclaw');
3010
- this.agentsLoading = true;
3011
- try {
3012
- const res = await api('get-openclaw-agents-file');
3013
- if (res.error) {
3014
- this.showMessage(res.error, 'error');
3015
- return;
3016
- }
3017
- if (res.configError) {
3018
- this.showMessage(`OpenClaw 配置解析失败,已使用默认 Workspace:${res.configError}`, 'error');
3019
- }
3020
- this.agentsContent = res.content || '';
3021
- this.agentsPath = res.path || '';
3022
- this.agentsExists = !!res.exists;
3023
- this.agentsLineEnding = res.lineEnding === '\r\n' ? '\r\n' : '\n';
3024
- this.showAgentsModal = true;
3025
- } catch (e) {
3026
- this.showMessage('加载 OpenClaw AGENTS.md 失败: ' + e.message, 'error');
3027
- } finally {
3028
- this.agentsLoading = false;
3029
- }
3030
- },
3031
-
3032
- setAgentsModalContext(context) {
3033
- this.agentsContext = context === 'openclaw' ? 'openclaw' : 'codex';
3034
- if (this.agentsContext === 'openclaw') {
3035
- this.agentsModalTitle = 'OpenClaw AGENTS.md 编辑器';
3036
- this.agentsModalHint = '保存后会写入 OpenClaw Workspace 下的 AGENTS.md。';
3037
- } else {
3038
- this.agentsModalTitle = 'AGENTS.md 编辑器';
3039
- this.agentsModalHint = '保存后会写入目标 AGENTS.md(与 config.toml 同级)。';
3040
- }
3041
- },
3042
-
3043
- closeAgentsModal() {
3044
- this.showAgentsModal = false;
3045
- this.agentsContent = '';
3046
- this.agentsPath = '';
3047
- this.agentsExists = false;
3048
- this.agentsLineEnding = '\n';
3049
- this.agentsSaving = false;
3050
- this.setAgentsModalContext('codex');
3051
- },
3052
-
3053
- async applyAgentsContent() {
3054
- this.agentsSaving = true;
3055
- try {
3056
- const action = this.agentsContext === 'openclaw'
3057
- ? 'apply-openclaw-agents-file'
3058
- : 'apply-agents-file';
3059
- const res = await api(action, {
3060
- content: this.agentsContent,
3061
- lineEnding: this.agentsLineEnding
3062
- });
3063
- if (res.error) {
3064
- this.showMessage(res.error, 'error');
4474
+ this.agentsLineEnding = res.lineEnding === '\r\n' ? '\r\n' : '\n';
4475
+ this.showAgentsModal = true;
4476
+ } catch (e) {
4477
+ this.showMessage('加载 AGENTS.md 失败: ' + e.message, 'error');
4478
+ } finally {
4479
+ this.agentsLoading = false;
4480
+ }
4481
+ },
4482
+
4483
+ async openOpenclawAgentsEditor() {
4484
+ this.setAgentsModalContext('openclaw');
4485
+ this.agentsLoading = true;
4486
+ try {
4487
+ const res = await api('get-openclaw-agents-file');
4488
+ if (res.error) {
4489
+ this.showMessage(res.error, 'error');
3065
4490
  return;
3066
4491
  }
3067
- this.showMessage('AGENTS.md 已保存', 'success');
3068
- this.closeAgentsModal();
4492
+ if (res.configError) {
4493
+ this.showMessage(`OpenClaw 配置解析失败,已使用默认 Workspace:${res.configError}`, 'error');
4494
+ }
4495
+ this.agentsContent = res.content || '';
4496
+ this.agentsPath = res.path || '';
4497
+ this.agentsExists = !!res.exists;
4498
+ this.agentsLineEnding = res.lineEnding === '\r\n' ? '\r\n' : '\n';
4499
+ this.showAgentsModal = true;
3069
4500
  } catch (e) {
3070
- this.showMessage('保存 AGENTS.md 失败: ' + e.message, 'error');
4501
+ this.showMessage('加载 OpenClaw AGENTS.md 失败: ' + e.message, 'error');
3071
4502
  } finally {
3072
- this.agentsSaving = false;
4503
+ this.agentsLoading = false;
3073
4504
  }
3074
4505
  },
3075
4506
 
3076
- async addProvider() {
3077
- if (!this.newProvider.name || !this.newProvider.url) {
3078
- return this.showMessage('名称和URL必填', 'error');
3079
- }
3080
- const name = this.newProvider.name.trim();
3081
- if (!name) {
3082
- return this.showMessage('名称不能为空', 'error');
4507
+ async openOpenclawWorkspaceEditor() {
4508
+ const fileName = (this.openclawWorkspaceFileName || '').trim();
4509
+ if (!fileName) {
4510
+ this.showMessage('请输入工作区文件名', 'error');
4511
+ return;
3083
4512
  }
3084
- if (this.providersList.some(item => item.name === name)) {
3085
- return this.showMessage('提供商已存在', 'error');
4513
+ this.setAgentsModalContext('openclaw-workspace', { fileName });
4514
+ this.agentsLoading = true;
4515
+ try {
4516
+ const res = await api('get-openclaw-workspace-file', { fileName });
4517
+ if (res.error) {
4518
+ this.showMessage(res.error, 'error');
4519
+ return;
4520
+ }
4521
+ if (res.configError) {
4522
+ this.showMessage(`OpenClaw 配置解析失败,已使用默认 Workspace:${res.configError}`, 'error');
4523
+ }
4524
+ this.agentsContent = res.content || '';
4525
+ this.agentsPath = res.path || '';
4526
+ this.agentsExists = !!res.exists;
4527
+ this.agentsLineEnding = res.lineEnding === '\r\n' ? '\r\n' : '\n';
4528
+ this.showAgentsModal = true;
4529
+ } catch (e) {
4530
+ this.showMessage('加载 OpenClaw 工作区文件失败: ' + e.message, 'error');
4531
+ } finally {
4532
+ this.agentsLoading = false;
4533
+ }
4534
+ },
4535
+
4536
+ setAgentsModalContext(context, options = {}) {
4537
+ if (context === 'openclaw-workspace') {
4538
+ const fileName = (options.fileName || this.openclawWorkspaceFileName || 'AGENTS.md').trim();
4539
+ this.agentsContext = 'openclaw-workspace';
4540
+ this.agentsWorkspaceFileName = fileName;
4541
+ this.agentsModalTitle = `OpenClaw 工作区文件: ${fileName}`;
4542
+ this.agentsModalHint = `保存后会写入 OpenClaw Workspace 下的 ${fileName}。`;
4543
+ return;
4544
+ }
4545
+ this.agentsContext = context === 'openclaw' ? 'openclaw' : 'codex';
4546
+ if (this.agentsContext === 'openclaw') {
4547
+ this.agentsModalTitle = 'OpenClaw AGENTS.md 编辑器';
4548
+ this.agentsModalHint = '保存后会写入 OpenClaw Workspace 下的 AGENTS.md。';
4549
+ } else {
4550
+ this.agentsModalTitle = 'AGENTS.md 编辑器';
4551
+ this.agentsModalHint = '保存后会写入目标 AGENTS.md(与 config.toml 同级)。';
4552
+ }
4553
+ this.agentsWorkspaceFileName = '';
4554
+ },
4555
+
4556
+ closeAgentsModal() {
4557
+ this.showAgentsModal = false;
4558
+ this.agentsContent = '';
4559
+ this.agentsPath = '';
4560
+ this.agentsExists = false;
4561
+ this.agentsLineEnding = '\n';
4562
+ this.agentsSaving = false;
4563
+ this.agentsWorkspaceFileName = '';
4564
+ this.setAgentsModalContext('codex');
4565
+ },
4566
+
4567
+ async applyAgentsContent() {
4568
+ this.agentsSaving = true;
4569
+ try {
4570
+ let action = 'apply-agents-file';
4571
+ const params = {
4572
+ content: this.agentsContent,
4573
+ lineEnding: this.agentsLineEnding
4574
+ };
4575
+ if (this.agentsContext === 'openclaw') {
4576
+ action = 'apply-openclaw-agents-file';
4577
+ } else if (this.agentsContext === 'openclaw-workspace') {
4578
+ action = 'apply-openclaw-workspace-file';
4579
+ params.fileName = this.agentsWorkspaceFileName;
4580
+ }
4581
+ const res = await api(action, params);
4582
+ if (res.error) {
4583
+ this.showMessage(res.error, 'error');
4584
+ return;
4585
+ }
4586
+ const successLabel = this.agentsContext === 'openclaw-workspace'
4587
+ ? `工作区文件已保存${this.agentsWorkspaceFileName ? `: ${this.agentsWorkspaceFileName}` : ''}`
4588
+ : (this.agentsContext === 'openclaw' ? 'OpenClaw AGENTS.md 已保存' : 'AGENTS.md 已保存');
4589
+ this.showMessage(successLabel, 'success');
4590
+ this.closeAgentsModal();
4591
+ } catch (e) {
4592
+ this.showMessage('保存文件失败: ' + e.message, 'error');
4593
+ } finally {
4594
+ this.agentsSaving = false;
4595
+ }
4596
+ },
4597
+
4598
+ async addProvider() {
4599
+ if (!this.newProvider.name || !this.newProvider.url) {
4600
+ return this.showMessage('名称和URL必填', 'error');
4601
+ }
4602
+ const name = this.newProvider.name.trim();
4603
+ if (!name) {
4604
+ return this.showMessage('名称不能为空', 'error');
4605
+ }
4606
+ if (this.providersList.some(item => item.name === name)) {
4607
+ return this.showMessage('提供商已存在', 'error');
3086
4608
  }
3087
4609
 
3088
4610
  const safeName = this.escapeTomlString(name);
@@ -3186,6 +4708,35 @@
3186
4708
 
3187
4709
  switchClaudeConfig(name) {
3188
4710
  this.currentClaudeConfig = name;
4711
+ this.refreshClaudeModelContext();
4712
+ },
4713
+
4714
+ onClaudeModelChange() {
4715
+ const name = this.currentClaudeConfig;
4716
+ if (!name) {
4717
+ this.showMessage('请先选择配置', 'error');
4718
+ return;
4719
+ }
4720
+ const model = (this.currentClaudeModel || '').trim();
4721
+ if (!model) {
4722
+ this.showMessage('请输入模型', 'error');
4723
+ return;
4724
+ }
4725
+ const existing = this.claudeConfigs[name] || {};
4726
+ this.currentClaudeModel = model;
4727
+ this.claudeConfigs[name] = {
4728
+ apiKey: existing.apiKey || '',
4729
+ baseUrl: existing.baseUrl || '',
4730
+ model: model,
4731
+ hasKey: !!existing.apiKey
4732
+ };
4733
+ this.saveClaudeConfigs();
4734
+ this.updateClaudeModelsCurrent();
4735
+ if (!this.claudeConfigs[name].apiKey) {
4736
+ this.showMessage('该配置未设置 API Key,请先编辑', 'error');
4737
+ return;
4738
+ }
4739
+ this.applyClaudeConfig(name);
3189
4740
  },
3190
4741
 
3191
4742
  saveClaudeConfigs() {
@@ -3214,6 +4765,9 @@
3214
4765
  this.saveClaudeConfigs();
3215
4766
  this.showMessage('配置已更新', 'success');
3216
4767
  this.closeEditConfigModal();
4768
+ if (name === this.currentClaudeConfig) {
4769
+ this.refreshClaudeModelContext();
4770
+ }
3217
4771
  },
3218
4772
 
3219
4773
  closeEditConfigModal() {
@@ -3233,7 +4787,12 @@
3233
4787
 
3234
4788
  const config = this.claudeConfigs[name];
3235
4789
  if (!config.apiKey) {
3236
- return this.showMessage('请先输入 API Key', 'error');
4790
+ this.showMessage('已保存,未应用:请先输入 API Key', 'info');
4791
+ this.closeEditConfigModal();
4792
+ if (name === this.currentClaudeConfig) {
4793
+ this.refreshClaudeModelContext();
4794
+ }
4795
+ return;
3237
4796
  }
3238
4797
 
3239
4798
  const res = await api('apply-claude-config', { config });
@@ -3243,30 +4802,9 @@
3243
4802
  const targetTip = res.targetPath ? `(${res.targetPath})` : '';
3244
4803
  this.showMessage(`已保存并应用到 Claude 配置${targetTip}`, 'success');
3245
4804
  this.closeEditConfigModal();
3246
- }
3247
- },
3248
-
3249
- async saveAndApplyEnvCompat() {
3250
- const name = this.editingConfig.name;
3251
- this.claudeConfigs[name] = {
3252
- apiKey: this.editingConfig.apiKey,
3253
- baseUrl: this.editingConfig.baseUrl,
3254
- model: this.editingConfig.model,
3255
- hasKey: !!this.editingConfig.apiKey
3256
- };
3257
- this.saveClaudeConfigs();
3258
-
3259
- const config = this.claudeConfigs[name];
3260
- if (!config.apiKey) {
3261
- return this.showMessage('请先输入 API Key', 'error');
3262
- }
3263
-
3264
- const res = await api('apply-env', { config });
3265
- if (res.error || res.success === false) {
3266
- this.showMessage(res.error || '应用环境变量失败', 'error');
3267
- } else {
3268
- this.showMessage('已保存并应用到系统环境变量(兼容模式)', 'success');
3269
- this.closeEditConfigModal();
4805
+ if (name === this.currentClaudeConfig) {
4806
+ this.refreshClaudeModelContext();
4807
+ }
3270
4808
  }
3271
4809
  },
3272
4810
 
@@ -3290,6 +4828,7 @@
3290
4828
  this.saveClaudeConfigs();
3291
4829
  this.showMessage('配置已添加', 'success');
3292
4830
  this.closeClaudeConfigModal();
4831
+ this.refreshClaudeModelContext();
3293
4832
  },
3294
4833
 
3295
4834
  deleteClaudeConfig(name) {
@@ -3305,10 +4844,12 @@
3305
4844
  }
3306
4845
  this.saveClaudeConfigs();
3307
4846
  this.showMessage('配置已删除', 'success');
4847
+ this.refreshClaudeModelContext();
3308
4848
  },
3309
4849
 
3310
4850
  async applyClaudeConfig(name) {
3311
4851
  this.currentClaudeConfig = name;
4852
+ this.refreshClaudeModelContext();
3312
4853
  const config = this.claudeConfigs[name];
3313
4854
 
3314
4855
  if (!config.apiKey) {
@@ -3324,203 +4865,1049 @@
3324
4865
  }
3325
4866
  },
3326
4867
 
3327
- closeClaudeConfigModal() {
3328
- this.showClaudeConfigModal = false;
3329
- this.newClaudeConfig = {
3330
- name: '',
3331
- apiKey: '',
3332
- baseUrl: 'https://open.bigmodel.cn/api/anthropic',
3333
- model: 'glm-4.7'
3334
- };
3335
- },
3336
-
3337
- openclawHasContent(config) {
3338
- return !!(config && typeof config.content === 'string' && config.content.trim());
3339
- },
3340
-
3341
- openclawSubtitle(config) {
3342
- if (!this.openclawHasContent(config)) {
3343
- return '未设置配置';
3344
- }
3345
- const length = config.content.trim().length;
3346
- return `已保存 ${length} 字符`;
3347
- },
3348
-
3349
- saveOpenclawConfigs() {
3350
- localStorage.setItem('openclawConfigs', JSON.stringify(this.openclawConfigs));
3351
- },
3352
-
3353
- openOpenclawAddModal() {
3354
- this.openclawEditorTitle = '添加 OpenClaw 配置';
3355
- this.openclawEditing = {
3356
- name: '',
3357
- content: DEFAULT_OPENCLAW_TEMPLATE,
3358
- lockName: false
3359
- };
3360
- this.openclawConfigPath = '';
3361
- this.openclawConfigExists = false;
3362
- this.openclawLineEnding = '\n';
3363
- this.showOpenclawConfigModal = true;
3364
- },
3365
-
3366
- openOpenclawEditModal(name) {
3367
- const config = this.openclawConfigs[name];
3368
- this.openclawEditorTitle = `编辑 OpenClaw 配置: ${name}`;
3369
- this.openclawEditing = {
3370
- name,
3371
- content: (config && config.content) ? config.content : '',
3372
- lockName: true
3373
- };
3374
- this.showOpenclawConfigModal = true;
3375
- },
3376
-
3377
- closeOpenclawConfigModal() {
3378
- this.showOpenclawConfigModal = false;
3379
- this.openclawEditing = { name: '', content: '', lockName: false };
3380
- this.openclawSaving = false;
3381
- this.openclawApplying = false;
3382
- },
3383
-
3384
- async loadOpenclawConfigFromFile() {
3385
- this.openclawFileLoading = true;
3386
- try {
3387
- const res = await api('get-openclaw-config');
3388
- if (res.error) {
3389
- this.showMessage(res.error, 'error');
3390
- return;
3391
- }
3392
- this.openclawConfigPath = res.path || '';
3393
- this.openclawConfigExists = !!res.exists;
3394
- this.openclawLineEnding = res.lineEnding === '\r\n' ? '\r\n' : '\n';
3395
- if (res.content && res.content.trim()) {
3396
- this.openclawEditing.content = res.content;
3397
- } else if (!this.openclawEditing.content) {
3398
- this.openclawEditing.content = DEFAULT_OPENCLAW_TEMPLATE;
3399
- }
3400
- this.showMessage('已加载当前 OpenClaw 配置', 'success');
3401
- } catch (e) {
3402
- this.showMessage('加载 OpenClaw 配置失败: ' + e.message, 'error');
3403
- } finally {
3404
- this.openclawFileLoading = false;
3405
- }
3406
- },
3407
-
3408
- persistOpenclawConfig({ closeModal = true } = {}) {
3409
- if (!this.openclawEditing.name || !this.openclawEditing.name.trim()) {
3410
- this.showMessage('请输入配置名称', 'error');
3411
- return '';
3412
- }
3413
- const name = this.openclawEditing.name.trim();
3414
- if (!this.openclawEditing.lockName && this.openclawConfigs[name]) {
3415
- this.showMessage('配置名称已存在', 'error');
3416
- return '';
3417
- }
3418
- if (!this.openclawEditing.content || !this.openclawEditing.content.trim()) {
3419
- this.showMessage('配置内容不能为空', 'error');
3420
- return '';
3421
- }
3422
-
3423
- this.openclawConfigs[name] = {
3424
- content: this.openclawEditing.content
3425
- };
3426
- this.currentOpenclawConfig = name;
3427
- this.saveOpenclawConfigs();
3428
- if (closeModal) {
3429
- this.closeOpenclawConfigModal();
3430
- }
3431
- return name;
3432
- },
3433
-
3434
- async saveOpenclawConfig() {
3435
- this.openclawSaving = true;
3436
- try {
3437
- const name = this.persistOpenclawConfig();
3438
- if (!name) return;
3439
- this.showMessage('OpenClaw 配置已保存', 'success');
3440
- } finally {
3441
- this.openclawSaving = false;
3442
- }
3443
- },
3444
-
3445
- async saveAndApplyOpenclawConfig() {
3446
- this.openclawApplying = true;
3447
- try {
3448
- const name = this.persistOpenclawConfig({ closeModal: false });
3449
- if (!name) return;
3450
- const config = this.openclawConfigs[name];
3451
- const res = await api('apply-openclaw-config', {
3452
- content: config.content,
3453
- lineEnding: this.openclawLineEnding
3454
- });
3455
- if (res.error || res.success === false) {
3456
- this.showMessage(res.error || '应用 OpenClaw 配置失败', 'error');
3457
- return;
3458
- }
3459
- this.openclawConfigPath = res.targetPath || this.openclawConfigPath;
3460
- this.openclawConfigExists = true;
3461
- const targetTip = res.targetPath ? `(${res.targetPath})` : '';
3462
- this.showMessage(`已保存并应用 OpenClaw 配置${targetTip}`, 'success');
3463
- this.closeOpenclawConfigModal();
3464
- } catch (e) {
3465
- this.showMessage('应用 OpenClaw 配置失败: ' + e.message, 'error');
3466
- } finally {
3467
- this.openclawApplying = false;
3468
- }
3469
- },
3470
-
3471
- deleteOpenclawConfig(name) {
3472
- if (Object.keys(this.openclawConfigs).length <= 1) {
3473
- return this.showMessage('至少保留一个配置', 'error');
3474
- }
3475
- if (!confirm(`确定删除配置 "${name}"?`)) return;
3476
- delete this.openclawConfigs[name];
3477
- if (this.currentOpenclawConfig === name) {
3478
- this.currentOpenclawConfig = Object.keys(this.openclawConfigs)[0];
3479
- }
3480
- this.saveOpenclawConfigs();
3481
- this.showMessage('OpenClaw 配置已删除', 'success');
3482
- },
3483
-
3484
- async applyOpenclawConfig(name) {
3485
- this.currentOpenclawConfig = name;
3486
- const config = this.openclawConfigs[name];
3487
- if (!this.openclawHasContent(config)) {
3488
- return this.showMessage('该配置为空,请先编辑', 'error');
3489
- }
3490
- const res = await api('apply-openclaw-config', {
3491
- content: config.content,
3492
- lineEnding: this.openclawLineEnding
3493
- });
3494
- if (res.error || res.success === false) {
3495
- this.showMessage(res.error || '应用 OpenClaw 配置失败', 'error');
3496
- } else {
3497
- this.openclawConfigPath = res.targetPath || this.openclawConfigPath;
3498
- this.openclawConfigExists = true;
3499
- const targetTip = res.targetPath ? `(${res.targetPath})` : '';
3500
- this.showMessage(`已应用 OpenClaw 配置: ${name}${targetTip}`, 'success');
3501
- }
3502
- },
3503
-
3504
- formatLatency(result) {
3505
- if (!result) return '';
3506
- if (!result.ok) return result.status ? `ERR ${result.status}` : 'ERR';
3507
- const ms = typeof result.durationMs === 'number' ? result.durationMs : 0;
4868
+ closeClaudeConfigModal() {
4869
+ this.showClaudeConfigModal = false;
4870
+ this.newClaudeConfig = {
4871
+ name: '',
4872
+ apiKey: '',
4873
+ baseUrl: 'https://open.bigmodel.cn/api/anthropic',
4874
+ model: 'glm-4.7'
4875
+ };
4876
+ },
4877
+
4878
+ getOpenclawParser() {
4879
+ if (window.JSON5 && typeof window.JSON5.parse === 'function' && typeof window.JSON5.stringify === 'function') {
4880
+ return {
4881
+ parse: window.JSON5.parse,
4882
+ stringify: window.JSON5.stringify
4883
+ };
4884
+ }
4885
+ return {
4886
+ parse: JSON.parse,
4887
+ stringify: JSON.stringify
4888
+ };
4889
+ },
4890
+
4891
+ parseOpenclawContent(content, options = {}) {
4892
+ const allowEmpty = !!options.allowEmpty;
4893
+ const raw = typeof content === 'string' ? content.trim() : '';
4894
+ if (!raw) {
4895
+ if (allowEmpty) {
4896
+ return { ok: true, data: {} };
4897
+ }
4898
+ return { ok: false, error: '配置内容为空' };
4899
+ }
4900
+ try {
4901
+ const parser = this.getOpenclawParser();
4902
+ const data = parser.parse(raw);
4903
+ if (!data || typeof data !== 'object' || Array.isArray(data)) {
4904
+ return { ok: false, error: '配置格式错误(根节点必须是对象)' };
4905
+ }
4906
+ return { ok: true, data };
4907
+ } catch (e) {
4908
+ return { ok: false, error: e.message || '解析失败' };
4909
+ }
4910
+ },
4911
+
4912
+ stringifyOpenclawConfig(data) {
4913
+ const parser = this.getOpenclawParser();
4914
+ try {
4915
+ return parser.stringify(data, null, 2);
4916
+ } catch (e) {
4917
+ return JSON.stringify(data, null, 2);
4918
+ }
4919
+ },
4920
+
4921
+ resetOpenclawStructured() {
4922
+ this.openclawStructured = {
4923
+ agentPrimary: '',
4924
+ agentFallbacks: [''],
4925
+ workspace: '',
4926
+ timeout: '',
4927
+ contextTokens: '',
4928
+ maxConcurrent: '',
4929
+ envItems: [{ key: '', value: '', show: false }],
4930
+ toolsProfile: 'default',
4931
+ toolsAllow: [''],
4932
+ toolsDeny: ['']
4933
+ };
4934
+ this.openclawAgentsList = [];
4935
+ this.openclawProviders = [];
4936
+ this.openclawMissingProviders = [];
4937
+ },
4938
+
4939
+ getOpenclawQuickDefaults() {
4940
+ return {
4941
+ providerName: '',
4942
+ baseUrl: '',
4943
+ apiKey: '',
4944
+ apiType: 'openai-responses',
4945
+ modelId: '',
4946
+ modelName: '',
4947
+ contextWindow: '',
4948
+ maxTokens: '',
4949
+ setPrimary: true,
4950
+ overrideProvider: true,
4951
+ overrideModels: true,
4952
+ showKey: false
4953
+ };
4954
+ },
4955
+
4956
+ resetOpenclawQuick() {
4957
+ this.openclawQuick = this.getOpenclawQuickDefaults();
4958
+ },
4959
+
4960
+ toggleOpenclawQuickKey() {
4961
+ this.openclawQuick.showKey = !this.openclawQuick.showKey;
4962
+ },
4963
+
4964
+ fillOpenclawQuickFromConfig(config) {
4965
+ const defaults = this.getOpenclawQuickDefaults();
4966
+ if (!config || typeof config !== 'object' || Array.isArray(config)) {
4967
+ this.openclawQuick = defaults;
4968
+ return;
4969
+ }
4970
+
4971
+ const agentDefaults = config.agents && typeof config.agents === 'object' && !Array.isArray(config.agents)
4972
+ && config.agents.defaults && typeof config.agents.defaults === 'object' && !Array.isArray(config.agents.defaults)
4973
+ ? config.agents.defaults
4974
+ : {};
4975
+ const modelConfig = agentDefaults.model;
4976
+ const legacyAgent = config.agent && typeof config.agent === 'object' && !Array.isArray(config.agent)
4977
+ ? config.agent
4978
+ : {};
4979
+
4980
+ let primaryRef = '';
4981
+ if (modelConfig && typeof modelConfig === 'object' && !Array.isArray(modelConfig) && typeof modelConfig.primary === 'string') {
4982
+ primaryRef = modelConfig.primary;
4983
+ } else if (typeof modelConfig === 'string') {
4984
+ primaryRef = modelConfig;
4985
+ }
4986
+ if (!primaryRef) {
4987
+ if (typeof legacyAgent.model === 'string') {
4988
+ primaryRef = legacyAgent.model;
4989
+ } else if (legacyAgent.model && typeof legacyAgent.model === 'object' && typeof legacyAgent.model.primary === 'string') {
4990
+ primaryRef = legacyAgent.model.primary;
4991
+ }
4992
+ }
4993
+
4994
+ let providerName = '';
4995
+ let modelId = '';
4996
+ if (primaryRef) {
4997
+ const parts = primaryRef.split('/');
4998
+ if (parts.length >= 2) {
4999
+ providerName = parts.shift().trim();
5000
+ modelId = parts.join('/').trim();
5001
+ }
5002
+ }
5003
+
5004
+ const providers = config.models && typeof config.models === 'object' && !Array.isArray(config.models)
5005
+ && config.models.providers && typeof config.models.providers === 'object' && !Array.isArray(config.models.providers)
5006
+ ? config.models.providers
5007
+ : null;
5008
+ let providerConfig = providerName && providers ? providers[providerName] : null;
5009
+ if (!providerName && providers) {
5010
+ const providerKeys = Object.keys(providers);
5011
+ if (providerKeys.length === 1) {
5012
+ providerName = providerKeys[0];
5013
+ providerConfig = providers[providerName];
5014
+ }
5015
+ }
5016
+
5017
+ let modelEntry = null;
5018
+ if (providerConfig && typeof providerConfig === 'object' && Array.isArray(providerConfig.models)) {
5019
+ if (modelId) {
5020
+ modelEntry = providerConfig.models.find(item => item && item.id === modelId);
5021
+ }
5022
+ if (!modelEntry && providerConfig.models.length === 1) {
5023
+ modelEntry = providerConfig.models[0];
5024
+ if (!modelId && modelEntry && typeof modelEntry.id === 'string') {
5025
+ modelId = modelEntry.id;
5026
+ }
5027
+ }
5028
+ }
5029
+
5030
+ const baseUrl = providerConfig && typeof providerConfig === 'object' && typeof providerConfig.baseUrl === 'string'
5031
+ ? providerConfig.baseUrl
5032
+ : '';
5033
+ const apiKey = providerConfig && typeof providerConfig === 'object' && typeof providerConfig.apiKey === 'string'
5034
+ ? providerConfig.apiKey
5035
+ : '';
5036
+ const apiType = providerConfig && typeof providerConfig === 'object' && typeof providerConfig.api === 'string'
5037
+ ? providerConfig.api
5038
+ : defaults.apiType;
5039
+
5040
+ this.openclawQuick = {
5041
+ ...defaults,
5042
+ providerName,
5043
+ baseUrl,
5044
+ apiKey,
5045
+ apiType,
5046
+ modelId: modelId || '',
5047
+ modelName: modelEntry && typeof modelEntry.name === 'string' ? modelEntry.name : '',
5048
+ contextWindow: modelEntry && typeof modelEntry.contextWindow === 'number'
5049
+ ? String(modelEntry.contextWindow)
5050
+ : '',
5051
+ maxTokens: modelEntry && typeof modelEntry.maxTokens === 'number'
5052
+ ? String(modelEntry.maxTokens)
5053
+ : ''
5054
+ };
5055
+ },
5056
+
5057
+ syncOpenclawQuickFromText(options = {}) {
5058
+ const silent = !!options.silent;
5059
+ const parsed = this.parseOpenclawContent(this.openclawEditing.content, { allowEmpty: true });
5060
+ if (!parsed.ok) {
5061
+ this.resetOpenclawQuick();
5062
+ if (!silent) {
5063
+ this.showMessage('解析 OpenClaw 配置失败: ' + parsed.error, 'error');
5064
+ }
5065
+ return false;
5066
+ }
5067
+ this.fillOpenclawQuickFromConfig(parsed.data);
5068
+ if (!silent) {
5069
+ this.showMessage('已从编辑器读取快速配置', 'success');
5070
+ }
5071
+ return true;
5072
+ },
5073
+
5074
+ mergeOpenclawModelEntry(existing, incoming, overwrite = false) {
5075
+ if (!existing || typeof existing !== 'object' || Array.isArray(existing)) {
5076
+ return { ...incoming };
5077
+ }
5078
+ if (overwrite) {
5079
+ return { ...incoming };
5080
+ }
5081
+ const merged = { ...existing };
5082
+ for (const [key, value] of Object.entries(incoming || {})) {
5083
+ if (merged[key] === undefined || merged[key] === null || merged[key] === '') {
5084
+ merged[key] = value;
5085
+ }
5086
+ }
5087
+ return merged;
5088
+ },
5089
+
5090
+ fillOpenclawStructured(config) {
5091
+ const defaults = config && config.agents && typeof config.agents === 'object' && !Array.isArray(config.agents)
5092
+ && config.agents.defaults && typeof config.agents.defaults === 'object' && !Array.isArray(config.agents.defaults)
5093
+ ? config.agents.defaults
5094
+ : {};
5095
+ const model = defaults.model && typeof defaults.model === 'object' && !Array.isArray(defaults.model)
5096
+ ? defaults.model
5097
+ : {};
5098
+ const legacyAgent = config && config.agent && typeof config.agent === 'object' && !Array.isArray(config.agent)
5099
+ ? config.agent
5100
+ : {};
5101
+ const fallbackList = Array.isArray(model.fallbacks)
5102
+ ? model.fallbacks.filter(item => typeof item === 'string' && item.trim()).map(item => item.trim())
5103
+ : [];
5104
+ const env = config && config.env && typeof config.env === 'object' && !Array.isArray(config.env)
5105
+ ? config.env
5106
+ : {};
5107
+ const envItems = Object.entries(env).map(([key, value]) => ({
5108
+ key,
5109
+ value: value == null ? '' : String(value),
5110
+ show: false
5111
+ }));
5112
+ const tools = config && config.tools && typeof config.tools === 'object' && !Array.isArray(config.tools)
5113
+ ? config.tools
5114
+ : {};
5115
+
5116
+ let primary = typeof model.primary === 'string' ? model.primary : '';
5117
+ if (!primary) {
5118
+ if (typeof legacyAgent.model === 'string') {
5119
+ primary = legacyAgent.model;
5120
+ } else if (legacyAgent.model && typeof legacyAgent.model === 'object' && typeof legacyAgent.model.primary === 'string') {
5121
+ primary = legacyAgent.model.primary;
5122
+ }
5123
+ }
5124
+
5125
+ this.openclawStructured = {
5126
+ agentPrimary: primary,
5127
+ agentFallbacks: fallbackList.length ? fallbackList : [''],
5128
+ workspace: typeof defaults.workspace === 'string' ? defaults.workspace : '',
5129
+ timeout: typeof defaults.timeout === 'number' && Number.isFinite(defaults.timeout)
5130
+ ? String(defaults.timeout)
5131
+ : '',
5132
+ contextTokens: typeof defaults.contextTokens === 'number' && Number.isFinite(defaults.contextTokens)
5133
+ ? String(defaults.contextTokens)
5134
+ : '',
5135
+ maxConcurrent: typeof defaults.maxConcurrent === 'number' && Number.isFinite(defaults.maxConcurrent)
5136
+ ? String(defaults.maxConcurrent)
5137
+ : '',
5138
+ envItems: envItems.length ? envItems : [{ key: '', value: '', show: false }],
5139
+ toolsProfile: typeof tools.profile === 'string' && tools.profile.trim() ? tools.profile : 'default',
5140
+ toolsAllow: Array.isArray(tools.allow) && tools.allow.length
5141
+ ? tools.allow.filter(item => typeof item === 'string' && item.trim()).map(item => item.trim())
5142
+ : [''],
5143
+ toolsDeny: Array.isArray(tools.deny) && tools.deny.length
5144
+ ? tools.deny.filter(item => typeof item === 'string' && item.trim()).map(item => item.trim())
5145
+ : ['']
5146
+ };
5147
+ },
5148
+
5149
+ syncOpenclawStructuredFromText(options = {}) {
5150
+ const silent = !!options.silent;
5151
+ const parsed = this.parseOpenclawContent(this.openclawEditing.content, { allowEmpty: true });
5152
+ if (!parsed.ok) {
5153
+ this.resetOpenclawStructured();
5154
+ this.resetOpenclawQuick();
5155
+ if (!silent) {
5156
+ this.showMessage('解析 OpenClaw 配置失败: ' + parsed.error, 'error');
5157
+ }
5158
+ return false;
5159
+ }
5160
+ this.fillOpenclawStructured(parsed.data);
5161
+ this.fillOpenclawQuickFromConfig(parsed.data);
5162
+ this.refreshOpenclawProviders(parsed.data);
5163
+ this.refreshOpenclawAgentsList(parsed.data);
5164
+ if (!silent) {
5165
+ this.showMessage('已从文本刷新结构化配置', 'success');
5166
+ }
5167
+ return true;
5168
+ },
5169
+
5170
+ getOpenclawActiveProviders(config) {
5171
+ const active = new Set();
5172
+ const addProvider = (ref) => {
5173
+ if (typeof ref !== 'string') return;
5174
+ const text = ref.trim();
5175
+ if (!text) return;
5176
+ const parts = text.split('/');
5177
+ if (parts.length < 2) return;
5178
+ const provider = parts[0].trim();
5179
+ if (provider) active.add(provider);
5180
+ };
5181
+ const defaults = config && config.agents && config.agents.defaults
5182
+ ? config.agents.defaults
5183
+ : {};
5184
+ const model = defaults && defaults.model;
5185
+ if (model && typeof model === 'object' && !Array.isArray(model)) {
5186
+ addProvider(model.primary);
5187
+ if (Array.isArray(model.fallbacks)) {
5188
+ for (const item of model.fallbacks) {
5189
+ addProvider(item);
5190
+ }
5191
+ }
5192
+ } else if (typeof model === 'string') {
5193
+ addProvider(model);
5194
+ }
5195
+ const modelsDefaults = config && config.models && config.models.defaults
5196
+ ? config.models.defaults
5197
+ : {};
5198
+ if (modelsDefaults && typeof modelsDefaults.provider === 'string' && modelsDefaults.provider.trim()) {
5199
+ active.add(modelsDefaults.provider.trim());
5200
+ }
5201
+ if (modelsDefaults && typeof modelsDefaults.model === 'string') {
5202
+ addProvider(modelsDefaults.model);
5203
+ }
5204
+ return active;
5205
+ },
5206
+
5207
+ maskProviderValue(value) {
5208
+ const text = value == null ? '' : String(value);
5209
+ if (!text) return '****';
5210
+ if (text.length <= 6) return '****';
5211
+ return `${text.slice(0, 3)}****${text.slice(-3)}`;
5212
+ },
5213
+
5214
+ formatProviderValue(key, value) {
5215
+ if (typeof value === 'undefined' || value === null) {
5216
+ return '';
5217
+ }
5218
+ let text = '';
5219
+ if (typeof value === 'string') {
5220
+ text = value;
5221
+ } else if (typeof value === 'number' || typeof value === 'boolean') {
5222
+ text = String(value);
5223
+ } else {
5224
+ try {
5225
+ text = JSON.stringify(value);
5226
+ } catch (_) {
5227
+ text = String(value);
5228
+ }
5229
+ }
5230
+ if (!text) return '';
5231
+ if (/key|token|secret|password/i.test(key)) {
5232
+ return this.maskProviderValue(text);
5233
+ }
5234
+ if (text.length > 160) {
5235
+ return `${text.slice(0, 157)}...`;
5236
+ }
5237
+ return text;
5238
+ },
5239
+
5240
+ collectOpenclawProviders(source, providerMap, activeProviders, entries) {
5241
+ if (!providerMap || typeof providerMap !== 'object' || Array.isArray(providerMap)) {
5242
+ return;
5243
+ }
5244
+ const keys = Object.keys(providerMap).sort();
5245
+ for (const key of keys) {
5246
+ const value = providerMap[key];
5247
+ const fields = [];
5248
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
5249
+ const fieldKeys = Object.keys(value).sort();
5250
+ for (const fieldKey of fieldKeys) {
5251
+ const fieldValue = this.formatProviderValue(fieldKey, value[fieldKey]);
5252
+ if (fieldValue === '') continue;
5253
+ fields.push({ key: fieldKey, value: fieldValue });
5254
+ }
5255
+ } else {
5256
+ const fieldValue = this.formatProviderValue('value', value);
5257
+ if (fieldValue !== '') {
5258
+ fields.push({ key: 'value', value: fieldValue });
5259
+ }
5260
+ }
5261
+ entries.push({
5262
+ key,
5263
+ source,
5264
+ fields,
5265
+ isActive: activeProviders.has(key)
5266
+ });
5267
+ }
5268
+ },
5269
+
5270
+ refreshOpenclawProviders(config) {
5271
+ const activeProviders = this.getOpenclawActiveProviders(config || {});
5272
+ const entries = [];
5273
+ const modelsProviders = config && config.models ? config.models.providers : null;
5274
+ const rootProviders = config && config.providers ? config.providers : null;
5275
+ this.collectOpenclawProviders('models.providers', modelsProviders, activeProviders, entries);
5276
+ this.collectOpenclawProviders('providers', rootProviders, activeProviders, entries);
5277
+ const existing = new Set(entries.map(item => item.key));
5278
+ const missing = [];
5279
+ for (const provider of activeProviders) {
5280
+ if (!existing.has(provider)) {
5281
+ missing.push(provider);
5282
+ }
5283
+ }
5284
+ this.openclawProviders = entries;
5285
+ this.openclawMissingProviders = missing;
5286
+ },
5287
+
5288
+ refreshOpenclawAgentsList(config) {
5289
+ const list = config && config.agents && typeof config.agents === 'object' && !Array.isArray(config.agents)
5290
+ ? config.agents.list
5291
+ : null;
5292
+ if (!Array.isArray(list)) {
5293
+ this.openclawAgentsList = [];
5294
+ return;
5295
+ }
5296
+ const entries = [];
5297
+ list.forEach((item, index) => {
5298
+ if (!item || typeof item !== 'object') return;
5299
+ const id = typeof item.id === 'string' && item.id.trim() ? item.id.trim() : `agent-${index + 1}`;
5300
+ const identity = item.identity && typeof item.identity === 'object' && !Array.isArray(item.identity)
5301
+ ? item.identity
5302
+ : {};
5303
+ const name = typeof identity.name === 'string' && identity.name.trim()
5304
+ ? identity.name.trim()
5305
+ : id;
5306
+ entries.push({
5307
+ key: `${id}-${index}`,
5308
+ id,
5309
+ name,
5310
+ theme: typeof identity.theme === 'string' ? identity.theme : '',
5311
+ emoji: typeof identity.emoji === 'string' ? identity.emoji : '',
5312
+ avatar: typeof identity.avatar === 'string' ? identity.avatar : ''
5313
+ });
5314
+ });
5315
+ this.openclawAgentsList = entries;
5316
+ },
5317
+
5318
+ normalizeStringList(list) {
5319
+ if (!Array.isArray(list)) return [];
5320
+ const result = [];
5321
+ const seen = new Set();
5322
+ for (const item of list) {
5323
+ const value = typeof item === 'string' ? item.trim() : String(item || '').trim();
5324
+ if (!value) continue;
5325
+ const key = value;
5326
+ if (seen.has(key)) continue;
5327
+ seen.add(key);
5328
+ result.push(value);
5329
+ }
5330
+ return result;
5331
+ },
5332
+
5333
+ normalizeEnvItems(items) {
5334
+ if (!Array.isArray(items)) {
5335
+ return { ok: true, items: {} };
5336
+ }
5337
+ const output = {};
5338
+ const seen = new Set();
5339
+ for (const item of items) {
5340
+ const key = item && typeof item.key === 'string' ? item.key.trim() : '';
5341
+ if (!key) continue;
5342
+ if (seen.has(key)) {
5343
+ return { ok: false, error: `环境变量重复: ${key}` };
5344
+ }
5345
+ seen.add(key);
5346
+ const value = item && typeof item.value !== 'undefined' ? String(item.value) : '';
5347
+ output[key] = value;
5348
+ }
5349
+ return { ok: true, items: output };
5350
+ },
5351
+
5352
+ parseOptionalNumber(value, label) {
5353
+ const text = typeof value === 'string' ? value.trim() : String(value || '').trim();
5354
+ if (!text) {
5355
+ return { ok: true, value: null };
5356
+ }
5357
+ const num = Number(text);
5358
+ if (!Number.isFinite(num) || num < 0) {
5359
+ return { ok: false, error: `${label} 请输入有效数字` };
5360
+ }
5361
+ return { ok: true, value: num };
5362
+ },
5363
+
5364
+ applyOpenclawStructuredToText() {
5365
+ const parsed = this.parseOpenclawContent(this.openclawEditing.content, { allowEmpty: true });
5366
+ if (!parsed.ok) {
5367
+ this.showMessage('解析 OpenClaw 配置失败: ' + parsed.error, 'error');
5368
+ return;
5369
+ }
5370
+
5371
+ const config = parsed.data;
5372
+ const agents = config.agents && typeof config.agents === 'object' && !Array.isArray(config.agents)
5373
+ ? config.agents
5374
+ : {};
5375
+ const defaults = agents.defaults && typeof agents.defaults === 'object' && !Array.isArray(agents.defaults)
5376
+ ? agents.defaults
5377
+ : {};
5378
+ const model = defaults.model && typeof defaults.model === 'object' && !Array.isArray(defaults.model)
5379
+ ? defaults.model
5380
+ : {};
5381
+
5382
+ const primary = (this.openclawStructured.agentPrimary || '').trim();
5383
+ const fallbacks = this.normalizeStringList(this.openclawStructured.agentFallbacks);
5384
+ if (primary) {
5385
+ model.primary = primary;
5386
+ }
5387
+ if (fallbacks.length) {
5388
+ model.fallbacks = fallbacks;
5389
+ }
5390
+ if (primary || fallbacks.length) {
5391
+ defaults.model = model;
5392
+ }
5393
+ if (primary && config.agent && typeof config.agent === 'object' && !Array.isArray(config.agent)) {
5394
+ config.agent.model = primary;
5395
+ }
5396
+
5397
+ const workspace = (this.openclawStructured.workspace || '').trim();
5398
+ if (workspace) {
5399
+ defaults.workspace = workspace;
5400
+ }
5401
+
5402
+ const timeout = this.parseOptionalNumber(this.openclawStructured.timeout, 'Timeout');
5403
+ if (!timeout.ok) {
5404
+ this.showMessage(timeout.error, 'error');
5405
+ return;
5406
+ }
5407
+ if (timeout.value !== null) {
5408
+ defaults.timeout = timeout.value;
5409
+ }
5410
+
5411
+ const contextTokens = this.parseOptionalNumber(this.openclawStructured.contextTokens, 'Context Tokens');
5412
+ if (!contextTokens.ok) {
5413
+ this.showMessage(contextTokens.error, 'error');
5414
+ return;
5415
+ }
5416
+ if (contextTokens.value !== null) {
5417
+ defaults.contextTokens = contextTokens.value;
5418
+ }
5419
+
5420
+ const maxConcurrent = this.parseOptionalNumber(this.openclawStructured.maxConcurrent, 'Max Concurrent');
5421
+ if (!maxConcurrent.ok) {
5422
+ this.showMessage(maxConcurrent.error, 'error');
5423
+ return;
5424
+ }
5425
+ if (maxConcurrent.value !== null) {
5426
+ defaults.maxConcurrent = maxConcurrent.value;
5427
+ }
5428
+
5429
+ if (Object.keys(defaults).length > 0) {
5430
+ config.agents = agents;
5431
+ config.agents.defaults = defaults;
5432
+ }
5433
+
5434
+ const envResult = this.normalizeEnvItems(this.openclawStructured.envItems);
5435
+ if (!envResult.ok) {
5436
+ this.showMessage(envResult.error, 'error');
5437
+ return;
5438
+ }
5439
+ if (Object.keys(envResult.items).length > 0) {
5440
+ config.env = envResult.items;
5441
+ } else if (config.env) {
5442
+ delete config.env;
5443
+ }
5444
+
5445
+ const profile = (this.openclawStructured.toolsProfile || '').trim();
5446
+ const allowList = this.normalizeStringList(this.openclawStructured.toolsAllow);
5447
+ const denyList = this.normalizeStringList(this.openclawStructured.toolsDeny);
5448
+ const hasTools = profile || allowList.length || denyList.length || (config.tools && typeof config.tools === 'object');
5449
+ if (hasTools) {
5450
+ const tools = config.tools && typeof config.tools === 'object' && !Array.isArray(config.tools)
5451
+ ? config.tools
5452
+ : {};
5453
+ tools.profile = profile || tools.profile || 'default';
5454
+ tools.allow = allowList;
5455
+ tools.deny = denyList;
5456
+ config.tools = tools;
5457
+ }
5458
+
5459
+ this.openclawEditing.content = this.stringifyOpenclawConfig(config);
5460
+ this.refreshOpenclawProviders(config);
5461
+ this.refreshOpenclawAgentsList(config);
5462
+ this.fillOpenclawQuickFromConfig(config);
5463
+ this.showMessage('已写入编辑器', 'success');
5464
+ },
5465
+
5466
+ applyOpenclawQuickToText() {
5467
+ const parsed = this.parseOpenclawContent(this.openclawEditing.content, { allowEmpty: true });
5468
+ if (!parsed.ok) {
5469
+ this.showMessage('解析 OpenClaw 配置失败: ' + parsed.error, 'error');
5470
+ return;
5471
+ }
5472
+
5473
+ const providerName = (this.openclawQuick.providerName || '').trim();
5474
+ const modelId = (this.openclawQuick.modelId || '').trim();
5475
+ if (!providerName) {
5476
+ this.showMessage('请填写 Provider 名称', 'error');
5477
+ return;
5478
+ }
5479
+ if (providerName.includes('/')) {
5480
+ this.showMessage('Provider 名称不能包含 "/"', 'error');
5481
+ return;
5482
+ }
5483
+ if (!modelId) {
5484
+ this.showMessage('请填写模型 ID', 'error');
5485
+ return;
5486
+ }
5487
+
5488
+ const config = parsed.data;
5489
+ const ensureObject = (value) => (value && typeof value === 'object' && !Array.isArray(value)) ? value : {};
5490
+ const models = ensureObject(config.models);
5491
+ const providers = ensureObject(models.providers);
5492
+ const provider = ensureObject(providers[providerName]);
5493
+ const baseUrl = (this.openclawQuick.baseUrl || '').trim();
5494
+ if (!baseUrl && !provider.baseUrl) {
5495
+ this.showMessage('请填写 Base URL', 'error');
5496
+ return;
5497
+ }
5498
+
5499
+ const contextWindow = this.parseOptionalNumber(this.openclawQuick.contextWindow, '上下文长度');
5500
+ if (!contextWindow.ok) {
5501
+ this.showMessage(contextWindow.error, 'error');
5502
+ return;
5503
+ }
5504
+ const maxTokens = this.parseOptionalNumber(this.openclawQuick.maxTokens, '最大输出');
5505
+ if (!maxTokens.ok) {
5506
+ this.showMessage(maxTokens.error, 'error');
5507
+ return;
5508
+ }
5509
+
5510
+ const shouldOverrideProvider = !!this.openclawQuick.overrideProvider;
5511
+ const apiKey = (this.openclawQuick.apiKey || '').trim();
5512
+ const apiType = (this.openclawQuick.apiType || '').trim();
5513
+ const setProviderField = (key, value) => {
5514
+ if (!value) return;
5515
+ if (shouldOverrideProvider || provider[key] === undefined || provider[key] === null || provider[key] === '') {
5516
+ provider[key] = value;
5517
+ }
5518
+ };
5519
+ setProviderField('baseUrl', baseUrl);
5520
+ setProviderField('api', apiType);
5521
+ if (apiKey) {
5522
+ setProviderField('apiKey', apiKey);
5523
+ }
5524
+
5525
+ const modelName = (this.openclawQuick.modelName || '').trim() || modelId;
5526
+ const modelEntry = {
5527
+ id: modelId,
5528
+ name: modelName,
5529
+ reasoning: false,
5530
+ input: ['text'],
5531
+ cost: {
5532
+ input: 0,
5533
+ output: 0,
5534
+ cacheRead: 0,
5535
+ cacheWrite: 0
5536
+ }
5537
+ };
5538
+ if (contextWindow.value !== null) {
5539
+ modelEntry.contextWindow = contextWindow.value;
5540
+ }
5541
+ if (maxTokens.value !== null) {
5542
+ modelEntry.maxTokens = maxTokens.value;
5543
+ }
5544
+
5545
+ const existingModels = Array.isArray(provider.models) ? [...provider.models] : [];
5546
+ if (this.openclawQuick.overrideModels || existingModels.length === 0) {
5547
+ provider.models = [modelEntry];
5548
+ } else {
5549
+ const idx = existingModels.findIndex(item => item && item.id === modelId);
5550
+ if (idx >= 0) {
5551
+ existingModels[idx] = this.mergeOpenclawModelEntry(existingModels[idx], modelEntry, false);
5552
+ } else {
5553
+ existingModels.push(modelEntry);
5554
+ }
5555
+ provider.models = existingModels;
5556
+ }
5557
+
5558
+ providers[providerName] = provider;
5559
+ models.providers = providers;
5560
+ config.models = models;
5561
+
5562
+ if (this.openclawQuick.setPrimary) {
5563
+ const agents = ensureObject(config.agents);
5564
+ const defaults = ensureObject(agents.defaults);
5565
+ const modelConfig = defaults.model && typeof defaults.model === 'object' && !Array.isArray(defaults.model)
5566
+ ? defaults.model
5567
+ : {};
5568
+ modelConfig.primary = `${providerName}/${modelId}`;
5569
+ defaults.model = modelConfig;
5570
+ agents.defaults = defaults;
5571
+ config.agents = agents;
5572
+ if (config.agent && typeof config.agent === 'object' && !Array.isArray(config.agent)) {
5573
+ config.agent.model = modelConfig.primary;
5574
+ }
5575
+ }
5576
+
5577
+ this.openclawEditing.content = this.stringifyOpenclawConfig(config);
5578
+ this.fillOpenclawStructured(config);
5579
+ this.refreshOpenclawProviders(config);
5580
+ this.refreshOpenclawAgentsList(config);
5581
+ this.showMessage('快速配置已写入编辑器', 'success');
5582
+ },
5583
+
5584
+ addOpenclawFallback() {
5585
+ this.openclawStructured.agentFallbacks.push('');
5586
+ },
5587
+
5588
+ removeOpenclawFallback(index) {
5589
+ this.openclawStructured.agentFallbacks.splice(index, 1);
5590
+ if (this.openclawStructured.agentFallbacks.length === 0) {
5591
+ this.openclawStructured.agentFallbacks.push('');
5592
+ }
5593
+ },
5594
+
5595
+ addOpenclawEnvItem() {
5596
+ this.openclawStructured.envItems.push({ key: '', value: '', show: false });
5597
+ },
5598
+
5599
+ removeOpenclawEnvItem(index) {
5600
+ this.openclawStructured.envItems.splice(index, 1);
5601
+ if (this.openclawStructured.envItems.length === 0) {
5602
+ this.openclawStructured.envItems.push({ key: '', value: '', show: false });
5603
+ }
5604
+ },
5605
+
5606
+ toggleOpenclawEnvItem(index) {
5607
+ const item = this.openclawStructured.envItems[index];
5608
+ if (item) {
5609
+ item.show = !item.show;
5610
+ }
5611
+ },
5612
+
5613
+ addOpenclawToolsAllow() {
5614
+ this.openclawStructured.toolsAllow.push('');
5615
+ },
5616
+
5617
+ removeOpenclawToolsAllow(index) {
5618
+ this.openclawStructured.toolsAllow.splice(index, 1);
5619
+ if (this.openclawStructured.toolsAllow.length === 0) {
5620
+ this.openclawStructured.toolsAllow.push('');
5621
+ }
5622
+ },
5623
+
5624
+ addOpenclawToolsDeny() {
5625
+ this.openclawStructured.toolsDeny.push('');
5626
+ },
5627
+
5628
+ removeOpenclawToolsDeny(index) {
5629
+ this.openclawStructured.toolsDeny.splice(index, 1);
5630
+ if (this.openclawStructured.toolsDeny.length === 0) {
5631
+ this.openclawStructured.toolsDeny.push('');
5632
+ }
5633
+ },
5634
+
5635
+ openclawHasContent(config) {
5636
+ return !!(config && typeof config.content === 'string' && config.content.trim());
5637
+ },
5638
+
5639
+ openclawSubtitle(config) {
5640
+ if (!this.openclawHasContent(config)) {
5641
+ return '未设置配置';
5642
+ }
5643
+ const length = config.content.trim().length;
5644
+ return `已保存 ${length} 字符`;
5645
+ },
5646
+
5647
+ saveOpenclawConfigs() {
5648
+ localStorage.setItem('openclawConfigs', JSON.stringify(this.openclawConfigs));
5649
+ },
5650
+
5651
+ openOpenclawAddModal() {
5652
+ this.openclawEditorTitle = '添加 OpenClaw 配置';
5653
+ this.openclawEditing = {
5654
+ name: '',
5655
+ content: '',
5656
+ lockName: false
5657
+ };
5658
+ this.openclawConfigPath = '';
5659
+ this.openclawConfigExists = false;
5660
+ this.openclawLineEnding = '\n';
5661
+ void this.loadOpenclawConfigFromFile({ silent: true, force: true, fallbackToTemplate: true });
5662
+ this.showOpenclawConfigModal = true;
5663
+ },
5664
+
5665
+ openOpenclawEditModal(name) {
5666
+ this.openclawEditorTitle = `编辑 OpenClaw 配置: ${name}`;
5667
+ this.openclawEditing = {
5668
+ name,
5669
+ content: '',
5670
+ lockName: true
5671
+ };
5672
+ void this.loadOpenclawConfigFromFile({ silent: true, force: true, fallbackToTemplate: true });
5673
+ this.showOpenclawConfigModal = true;
5674
+ },
5675
+
5676
+ closeOpenclawConfigModal() {
5677
+ this.showOpenclawConfigModal = false;
5678
+ this.openclawEditing = { name: '', content: '', lockName: false };
5679
+ this.openclawSaving = false;
5680
+ this.openclawApplying = false;
5681
+ this.resetOpenclawStructured();
5682
+ this.resetOpenclawQuick();
5683
+ },
5684
+
5685
+ async loadOpenclawConfigFromFile(options = {}) {
5686
+ const silent = !!options.silent;
5687
+ const force = !!options.force;
5688
+ const fallbackToTemplate = options.fallbackToTemplate !== false;
5689
+ this.openclawFileLoading = true;
5690
+ try {
5691
+ const res = await api('get-openclaw-config');
5692
+ if (res.error) {
5693
+ if (!silent) {
5694
+ this.showMessage(res.error, 'error');
5695
+ }
5696
+ return;
5697
+ }
5698
+ this.openclawConfigPath = res.path || '';
5699
+ this.openclawConfigExists = !!res.exists;
5700
+ this.openclawLineEnding = res.lineEnding === '\r\n' ? '\r\n' : '\n';
5701
+ const hasContent = !!(res.content && res.content.trim());
5702
+ const shouldOverride = force || !this.openclawEditing.content || !this.openclawEditing.content.trim();
5703
+ if (hasContent && shouldOverride) {
5704
+ this.openclawEditing.content = res.content;
5705
+ } else if (!hasContent && shouldOverride && fallbackToTemplate) {
5706
+ this.openclawEditing.content = DEFAULT_OPENCLAW_TEMPLATE;
5707
+ }
5708
+ this.syncOpenclawStructuredFromText({ silent: true });
5709
+ if (!silent) {
5710
+ this.showMessage('已加载当前 OpenClaw 配置', 'success');
5711
+ }
5712
+ } catch (e) {
5713
+ if (!silent) {
5714
+ this.showMessage('加载 OpenClaw 配置失败: ' + e.message, 'error');
5715
+ }
5716
+ } finally {
5717
+ this.openclawFileLoading = false;
5718
+ }
5719
+ },
5720
+
5721
+ persistOpenclawConfig({ closeModal = true } = {}) {
5722
+ if (!this.openclawEditing.name || !this.openclawEditing.name.trim()) {
5723
+ this.showMessage('请输入配置名称', 'error');
5724
+ return '';
5725
+ }
5726
+ const name = this.openclawEditing.name.trim();
5727
+ if (!this.openclawEditing.lockName && this.openclawConfigs[name]) {
5728
+ this.showMessage('配置名称已存在', 'error');
5729
+ return '';
5730
+ }
5731
+ if (!this.openclawEditing.content || !this.openclawEditing.content.trim()) {
5732
+ this.showMessage('配置内容不能为空', 'error');
5733
+ return '';
5734
+ }
5735
+
5736
+ this.openclawConfigs[name] = {
5737
+ content: this.openclawEditing.content
5738
+ };
5739
+ this.currentOpenclawConfig = name;
5740
+ this.saveOpenclawConfigs();
5741
+ if (closeModal) {
5742
+ this.closeOpenclawConfigModal();
5743
+ }
5744
+ return name;
5745
+ },
5746
+
5747
+ async saveOpenclawConfig() {
5748
+ this.openclawSaving = true;
5749
+ try {
5750
+ const name = this.persistOpenclawConfig();
5751
+ if (!name) return;
5752
+ this.showMessage('OpenClaw 配置已保存', 'success');
5753
+ } finally {
5754
+ this.openclawSaving = false;
5755
+ }
5756
+ },
5757
+
5758
+ async saveAndApplyOpenclawConfig() {
5759
+ this.openclawApplying = true;
5760
+ try {
5761
+ const name = this.persistOpenclawConfig({ closeModal: false });
5762
+ if (!name) return;
5763
+ const config = this.openclawConfigs[name];
5764
+ const res = await api('apply-openclaw-config', {
5765
+ content: config.content,
5766
+ lineEnding: this.openclawLineEnding
5767
+ });
5768
+ if (res.error || res.success === false) {
5769
+ this.showMessage(res.error || '应用 OpenClaw 配置失败', 'error');
5770
+ return;
5771
+ }
5772
+ this.openclawConfigPath = res.targetPath || this.openclawConfigPath;
5773
+ this.openclawConfigExists = true;
5774
+ const targetTip = res.targetPath ? `(${res.targetPath})` : '';
5775
+ this.showMessage(`已保存并应用 OpenClaw 配置${targetTip}`, 'success');
5776
+ this.closeOpenclawConfigModal();
5777
+ } catch (e) {
5778
+ this.showMessage('应用 OpenClaw 配置失败: ' + e.message, 'error');
5779
+ } finally {
5780
+ this.openclawApplying = false;
5781
+ }
5782
+ },
5783
+
5784
+ deleteOpenclawConfig(name) {
5785
+ if (Object.keys(this.openclawConfigs).length <= 1) {
5786
+ return this.showMessage('至少保留一个配置', 'error');
5787
+ }
5788
+ if (!confirm(`确定删除配置 "${name}"?`)) return;
5789
+ delete this.openclawConfigs[name];
5790
+ if (this.currentOpenclawConfig === name) {
5791
+ this.currentOpenclawConfig = Object.keys(this.openclawConfigs)[0];
5792
+ }
5793
+ this.saveOpenclawConfigs();
5794
+ this.showMessage('OpenClaw 配置已删除', 'success');
5795
+ },
5796
+
5797
+ async applyOpenclawConfig(name) {
5798
+ this.currentOpenclawConfig = name;
5799
+ const config = this.openclawConfigs[name];
5800
+ if (!this.openclawHasContent(config)) {
5801
+ return this.showMessage('该配置为空,请先编辑', 'error');
5802
+ }
5803
+ const res = await api('apply-openclaw-config', {
5804
+ content: config.content,
5805
+ lineEnding: this.openclawLineEnding
5806
+ });
5807
+ if (res.error || res.success === false) {
5808
+ this.showMessage(res.error || '应用 OpenClaw 配置失败', 'error');
5809
+ } else {
5810
+ this.openclawConfigPath = res.targetPath || this.openclawConfigPath;
5811
+ this.openclawConfigExists = true;
5812
+ const targetTip = res.targetPath ? `(${res.targetPath})` : '';
5813
+ this.showMessage(`已应用 OpenClaw 配置: ${name}${targetTip}`, 'success');
5814
+ }
5815
+ },
5816
+
5817
+ formatLatency(result) {
5818
+ if (!result) return '';
5819
+ if (!result.ok) return result.status ? `ERR ${result.status}` : 'ERR';
5820
+ const ms = typeof result.durationMs === 'number' ? result.durationMs : 0;
3508
5821
  return `${ms}ms`;
3509
5822
  },
3510
5823
 
3511
- async runSpeedTest(name) {
3512
- if (!name || this.speedLoading[name]) return;
5824
+ buildSpeedTestIssue(name, result) {
5825
+ if (!name || !result) return null;
5826
+ if (result.error) {
5827
+ const error = String(result.error || '');
5828
+ const errorLower = error.toLowerCase();
5829
+ if (error === 'Provider not found') {
5830
+ return {
5831
+ code: 'remote-speedtest-provider-missing',
5832
+ message: `提供商 ${name} 未找到,无法测速`,
5833
+ suggestion: '检查配置是否存在该 provider'
5834
+ };
5835
+ }
5836
+ if (error === 'Provider missing URL' || error === 'Missing name or url') {
5837
+ return {
5838
+ code: 'remote-speedtest-baseurl-missing',
5839
+ message: `提供商 ${name} 缺少 base_url`,
5840
+ suggestion: '补全 base_url 后重试'
5841
+ };
5842
+ }
5843
+ if (errorLower.includes('invalid url')) {
5844
+ return {
5845
+ code: 'remote-speedtest-invalid-url',
5846
+ message: `提供商 ${name} 的 base_url 无效`,
5847
+ suggestion: '请设置为 http/https 的完整 URL'
5848
+ };
5849
+ }
5850
+ if (errorLower.includes('timeout')) {
5851
+ return {
5852
+ code: 'remote-speedtest-timeout',
5853
+ message: `提供商 ${name} 远程测速超时`,
5854
+ suggestion: '检查网络或 base_url 是否可达'
5855
+ };
5856
+ }
5857
+ return {
5858
+ code: 'remote-speedtest-unreachable',
5859
+ message: `提供商 ${name} 远程测速失败:${error || '无法连接'}`,
5860
+ suggestion: '检查网络或 base_url 是否可用'
5861
+ };
5862
+ }
5863
+
5864
+ const status = typeof result.status === 'number' ? result.status : 0;
5865
+ if (status === 401 || status === 403) {
5866
+ return {
5867
+ code: 'remote-speedtest-auth-failed',
5868
+ message: `提供商 ${name} 远程测速鉴权失败(401/403)`,
5869
+ suggestion: '检查 API Key 或认证方式'
5870
+ };
5871
+ }
5872
+ if (status >= 400) {
5873
+ return {
5874
+ code: 'remote-speedtest-http-error',
5875
+ message: `提供商 ${name} 远程测速返回异常状态: ${status}`,
5876
+ suggestion: '检查 base_url 或服务状态'
5877
+ };
5878
+ }
5879
+ return null;
5880
+ },
5881
+
5882
+ async runSpeedTest(name, options = {}) {
5883
+ if (!name || this.speedLoading[name]) return null;
5884
+ const silent = !!options.silent;
3513
5885
  this.speedLoading[name] = true;
3514
- const res = await api('speed-test', { name });
3515
- if (res.error) {
3516
- this.speedResults[name] = { ok: false, error: res.error };
3517
- this.showMessage(res.error, 'error');
3518
- } else {
5886
+ try {
5887
+ const res = await api('speed-test', { name });
5888
+ if (res.error) {
5889
+ this.speedResults[name] = { ok: false, error: res.error };
5890
+ if (!silent) {
5891
+ this.showMessage(res.error, 'error');
5892
+ }
5893
+ return { ok: false, error: res.error };
5894
+ }
3519
5895
  this.speedResults[name] = res;
3520
- const status = res.status ? ` (${res.status})` : '';
3521
- this.showMessage(`Speed ${name}: ${this.formatLatency(res)}${status}`, 'success');
5896
+ if (!silent) {
5897
+ const status = res.status ? ` (${res.status})` : '';
5898
+ this.showMessage(`Speed ${name}: ${this.formatLatency(res)}${status}`, 'success');
5899
+ }
5900
+ return res;
5901
+ } catch (e) {
5902
+ const message = e && e.message ? e.message : 'Speed test failed';
5903
+ this.speedResults[name] = { ok: false, error: message };
5904
+ if (!silent) {
5905
+ this.showMessage(message, 'error');
5906
+ }
5907
+ return { ok: false, error: message };
5908
+ } finally {
5909
+ this.speedLoading[name] = false;
3522
5910
  }
3523
- this.speedLoading[name] = false;
3524
5911
  },
3525
5912
 
3526
5913
  showMessage(text, type) {
@@ -3537,10 +5924,3 @@
3537
5924
  </script>
3538
5925
  </body>
3539
5926
  </html>
3540
-
3541
-
3542
-
3543
-
3544
-
3545
-
3546
-