codexmate 0.0.5 → 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
 
@@ -920,6 +921,51 @@
920
921
  letter-spacing: -0.01em;
921
922
  }
922
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
+
923
969
  .btn-session-refresh {
924
970
  border: 1px solid var(--color-border-soft);
925
971
  border-radius: var(--radius-sm);
@@ -947,13 +993,39 @@
947
993
  transform: none;
948
994
  }
949
995
 
950
- .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 {
951
1011
  border-color: var(--color-brand);
952
1012
  color: var(--color-brand);
953
1013
  transform: translateY(-1px);
954
1014
  }
955
1015
 
956
- .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 {
957
1029
  opacity: 0.5;
958
1030
  cursor: not-allowed;
959
1031
  transform: none;
@@ -990,11 +1062,38 @@
990
1062
  min-height: 520px;
991
1063
  }
992
1064
 
993
- .session-list {
994
- display: flex;
995
- flex-direction: column;
996
- gap: var(--spacing-xs);
997
- 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;
998
1097
  top: 12px;
999
1098
  height: 100%;
1000
1099
  max-height: none;
@@ -1108,6 +1207,21 @@
1108
1207
  transition: all var(--transition-fast) var(--ease-spring);
1109
1208
  }
1110
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
+
1111
1225
  .session-item-copy:hover {
1112
1226
  border-color: rgba(70, 86, 110, 0.7);
1113
1227
  background: rgba(70, 86, 110, 0.16);
@@ -1115,17 +1229,35 @@
1115
1229
  transform: translateY(-1px);
1116
1230
  }
1117
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
+
1118
1239
  .session-item-copy:disabled {
1119
1240
  opacity: 0.5;
1120
1241
  cursor: not-allowed;
1121
1242
  transform: none;
1122
1243
  }
1123
1244
 
1245
+ .session-item-delete:disabled {
1246
+ opacity: 0.5;
1247
+ cursor: not-allowed;
1248
+ transform: none;
1249
+ }
1250
+
1124
1251
  .session-item-copy svg {
1125
1252
  width: 16px;
1126
1253
  height: 16px;
1127
1254
  }
1128
1255
 
1256
+ .session-item-delete svg {
1257
+ width: 16px;
1258
+ height: 16px;
1259
+ }
1260
+
1129
1261
  .session-item-sub {
1130
1262
  font-size: var(--font-size-caption);
1131
1263
  color: var(--color-text-tertiary);
@@ -1461,6 +1593,9 @@
1461
1593
  background: linear-gradient(to bottom, var(--color-surface) 0%, rgba(255, 255, 255, 0.98) 100%);
1462
1594
  width: 90%;
1463
1595
  max-width: 400px;
1596
+ max-height: 90vh;
1597
+ overflow-y: auto;
1598
+ overscroll-behavior: contain;
1464
1599
  border-radius: var(--radius-lg);
1465
1600
  padding: var(--spacing-md);
1466
1601
  box-shadow: var(--shadow-modal);
@@ -1480,6 +1615,25 @@
1480
1615
  letter-spacing: -0.01em;
1481
1616
  }
1482
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
+
1483
1637
  .form-group {
1484
1638
  margin-bottom: var(--spacing-sm);
1485
1639
  }
@@ -1555,133 +1709,407 @@
1555
1709
  opacity: 0.8;
1556
1710
  }
1557
1711
 
1558
- .btn-group {
1559
- display: flex;
1560
- gap: var(--spacing-sm);
1712
+ .quick-section {
1561
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));
1562
1718
  }
1563
1719
 
1564
- .btn {
1565
- flex: 1;
1566
- padding: 14px var(--spacing-sm);
1567
- border-radius: var(--radius-sm);
1568
- font-size: var(--font-size-body);
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);
1727
+ }
1728
+
1729
+ .quick-title {
1730
+ font-size: var(--font-size-secondary);
1569
1731
  font-weight: var(--font-weight-secondary);
1570
- cursor: pointer;
1571
- transition: all var(--transition-fast) var(--ease-spring);
1572
- border: 1px solid var(--color-border-soft);
1573
- background: linear-gradient(to bottom, var(--color-surface) 0%, rgba(255, 255, 255, 0.95) 100%);
1574
1732
  color: var(--color-text-secondary);
1575
- box-shadow: var(--shadow-subtle);
1576
- letter-spacing: -0.01em;
1577
1733
  }
1578
1734
 
1579
- .btn:active {
1580
- transform: scale(0.97);
1735
+ .quick-actions {
1736
+ display: flex;
1737
+ flex-wrap: wrap;
1738
+ gap: var(--spacing-xs);
1581
1739
  }
1582
1740
 
1583
- .btn-cancel {
1584
- background: linear-gradient(to bottom, var(--color-bg) 0%, rgba(247, 241, 232, 0.8) 100%);
1585
- color: var(--color-text-primary);
1586
- border: 1px solid var(--color-border-soft);
1741
+ .quick-steps {
1742
+ display: flex;
1743
+ flex-wrap: wrap;
1744
+ gap: var(--spacing-xs);
1745
+ margin-bottom: var(--spacing-sm);
1587
1746
  }
1588
1747
 
1589
- .btn-cancel:hover {
1590
- background: linear-gradient(to bottom, var(--color-border) 0%, rgba(208, 196, 182, 0.5) 100%);
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);
1591
1758
  }
1592
1759
 
1593
- .btn-confirm {
1594
- background: linear-gradient(135deg, var(--color-brand) 0%, var(--color-brand-dark) 100%);
1595
- color: white;
1596
- box-shadow: 0 2px 4px rgba(210, 107, 90, 0.2);
1597
- border: none;
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);
1598
1771
  }
1599
1772
 
1600
- .btn-confirm:hover {
1601
- box-shadow: 0 4px 8px rgba(210, 107, 90, 0.25);
1602
- filter: brightness(1.05);
1773
+ .quick-grid {
1774
+ display: grid;
1775
+ gap: var(--spacing-sm);
1776
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
1603
1777
  }
1604
1778
 
1605
- .btn-confirm.secondary {
1606
- background: linear-gradient(135deg, var(--color-success) 0%, rgba(90, 139, 106, 0.85) 100%);
1607
- box-shadow: 0 2px 4px rgba(90, 139, 106, 0.2);
1608
- border: none;
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);
1609
1785
  }
1610
1786
 
1611
- .btn-confirm.secondary:hover {
1612
- box-shadow: 0 4px 8px rgba(90, 139, 106, 0.25);
1613
- filter: brightness(1.05);
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;
1614
1794
  }
1615
1795
 
1616
- /* ============================================
1617
- 模型列表
1618
- ============================================ */
1619
- .model-list {
1620
- max-height: 200px;
1621
- overflow-y: auto;
1622
- border: 1px solid rgba(208, 196, 182, 0.4);
1623
- border-radius: var(--radius-sm);
1624
- margin-bottom: var(--spacing-sm);
1625
- scrollbar-width: none;
1626
- background: linear-gradient(to bottom, var(--color-surface) 0%, rgba(255, 255, 255, 0.8) 100%);
1627
- box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.02);
1796
+ .quick-option input {
1797
+ accent-color: var(--color-brand);
1628
1798
  }
1629
1799
 
1630
- .model-list::-webkit-scrollbar {
1631
- display: none;
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);
1632
1806
  }
1633
1807
 
1634
- .model-item {
1808
+ .structured-header {
1635
1809
  display: flex;
1810
+ flex-wrap: wrap;
1811
+ gap: var(--spacing-xs);
1812
+ align-items: baseline;
1636
1813
  justify-content: space-between;
1637
- align-items: center;
1638
- padding: 11px var(--spacing-sm);
1639
- border-bottom: 1px solid rgba(208, 196, 182, 0.3);
1640
- font-size: var(--font-size-body);
1641
- color: var(--color-text-primary);
1642
- transition: all var(--transition-fast) var(--ease-spring);
1643
- letter-spacing: -0.005em;
1814
+ margin-bottom: var(--spacing-sm);
1644
1815
  }
1645
1816
 
1646
- .model-item:last-child {
1647
- border-bottom: none;
1817
+ .structured-title {
1818
+ font-size: var(--font-size-secondary);
1819
+ font-weight: var(--font-weight-secondary);
1820
+ color: var(--color-text-secondary);
1648
1821
  }
1649
1822
 
1650
- .model-item:hover {
1651
- background: linear-gradient(to right, rgba(247, 241, 232, 0.6) 0%, rgba(247, 241, 232, 0.3) 100%);
1823
+ .structured-grid {
1824
+ display: grid;
1825
+ gap: var(--spacing-sm);
1826
+ grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
1652
1827
  }
1653
1828
 
1654
- .btn-remove-model {
1655
- font-size: var(--font-size-caption);
1656
- font-weight: var(--font-weight-caption);
1657
- color: var(--color-text-tertiary);
1658
- cursor: pointer;
1659
- padding: 5px 10px;
1660
- border-radius: var(--radius-full);
1661
- transition: all var(--transition-fast) var(--ease-spring);
1662
- background: transparent;
1663
- border: 1px solid rgba(139, 118, 104, 0.2);
1664
- letter-spacing: 0.03em;
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);
1665
1835
  }
1666
1836
 
1667
- .btn-remove-model:hover {
1668
- background: linear-gradient(135deg, var(--color-error) 0%, rgba(200, 74, 58, 0.9) 100%);
1669
- color: white;
1670
- transform: scale(1.08);
1671
- box-shadow: 0 2px 6px rgba(200, 74, 58, 0.25);
1672
- border-color: transparent;
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;
1673
1842
  }
1674
1843
 
1675
- /* ============================================
1676
- Toast - 顶部横幅
1677
- ============================================ */
1678
- .toast {
1679
- position: fixed;
1680
- top: 16px;
1681
- left: 50%;
1682
- transform: translateX(-50%);
1683
- background: linear-gradient(to bottom, var(--color-surface) 0%, rgba(255, 255, 255, 0.95) 100%);
1684
- padding: 12px 24px;
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%);
2019
+ }
2020
+
2021
+ .btn-confirm {
2022
+ background: linear-gradient(135deg, var(--color-brand) 0%, var(--color-brand-dark) 100%);
2023
+ color: white;
2024
+ box-shadow: 0 2px 4px rgba(210, 107, 90, 0.2);
2025
+ border: none;
2026
+ }
2027
+
2028
+ .btn-confirm:hover {
2029
+ box-shadow: 0 4px 8px rgba(210, 107, 90, 0.25);
2030
+ filter: brightness(1.05);
2031
+ }
2032
+
2033
+ .btn-confirm.secondary {
2034
+ background: linear-gradient(135deg, var(--color-success) 0%, rgba(90, 139, 106, 0.85) 100%);
2035
+ box-shadow: 0 2px 4px rgba(90, 139, 106, 0.2);
2036
+ border: none;
2037
+ }
2038
+
2039
+ .btn-confirm.secondary:hover {
2040
+ box-shadow: 0 4px 8px rgba(90, 139, 106, 0.25);
2041
+ filter: brightness(1.05);
2042
+ }
2043
+
2044
+ /* ============================================
2045
+ 模型列表
2046
+ ============================================ */
2047
+ .model-list {
2048
+ max-height: 200px;
2049
+ overflow-y: auto;
2050
+ border: 1px solid rgba(208, 196, 182, 0.4);
2051
+ border-radius: var(--radius-sm);
2052
+ margin-bottom: var(--spacing-sm);
2053
+ scrollbar-width: none;
2054
+ background: linear-gradient(to bottom, var(--color-surface) 0%, rgba(255, 255, 255, 0.8) 100%);
2055
+ box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.02);
2056
+ }
2057
+
2058
+ .model-list::-webkit-scrollbar {
2059
+ display: none;
2060
+ }
2061
+
2062
+ .model-item {
2063
+ display: flex;
2064
+ justify-content: space-between;
2065
+ align-items: center;
2066
+ padding: 11px var(--spacing-sm);
2067
+ border-bottom: 1px solid rgba(208, 196, 182, 0.3);
2068
+ font-size: var(--font-size-body);
2069
+ color: var(--color-text-primary);
2070
+ transition: all var(--transition-fast) var(--ease-spring);
2071
+ letter-spacing: -0.005em;
2072
+ }
2073
+
2074
+ .model-item:last-child {
2075
+ border-bottom: none;
2076
+ }
2077
+
2078
+ .model-item:hover {
2079
+ background: linear-gradient(to right, rgba(247, 241, 232, 0.6) 0%, rgba(247, 241, 232, 0.3) 100%);
2080
+ }
2081
+
2082
+ .btn-remove-model {
2083
+ font-size: var(--font-size-caption);
2084
+ font-weight: var(--font-weight-caption);
2085
+ color: var(--color-text-tertiary);
2086
+ cursor: pointer;
2087
+ padding: 5px 10px;
2088
+ border-radius: var(--radius-full);
2089
+ transition: all var(--transition-fast) var(--ease-spring);
2090
+ background: transparent;
2091
+ border: 1px solid rgba(139, 118, 104, 0.2);
2092
+ letter-spacing: 0.03em;
2093
+ }
2094
+
2095
+ .btn-remove-model:hover {
2096
+ background: linear-gradient(135deg, var(--color-error) 0%, rgba(200, 74, 58, 0.9) 100%);
2097
+ color: white;
2098
+ transform: scale(1.08);
2099
+ box-shadow: 0 2px 6px rgba(200, 74, 58, 0.25);
2100
+ border-color: transparent;
2101
+ }
2102
+
2103
+ /* ============================================
2104
+ Toast - 顶部横幅
2105
+ ============================================ */
2106
+ .toast {
2107
+ position: fixed;
2108
+ top: 16px;
2109
+ left: 50%;
2110
+ transform: translateX(-50%);
2111
+ background: linear-gradient(to bottom, var(--color-surface) 0%, rgba(255, 255, 255, 0.95) 100%);
2112
+ padding: 12px 24px;
1685
2113
  border-radius: var(--radius-full);
1686
2114
  box-shadow: var(--shadow-raised);
1687
2115
  z-index: 200;
@@ -1813,14 +2241,14 @@
1813
2241
  <body>
1814
2242
  <div id="app" class="container" v-cloak>
1815
2243
  <!-- 主标题 -->
1816
- <h1 class="main-title">
2244
+ <h1 v-if="!sessionStandalone" class="main-title">
1817
2245
  Codex<br>
1818
2246
  <span class="accent">Mate.</span>
1819
2247
  </h1>
1820
- <p class="subtitle">本地配置中枢,统一管理 Codex / Claude Code / OpenClaw / 会话。</p>
2248
+ <p v-if="!sessionStandalone" class="subtitle">本地配置中枢,统一管理 Codex / Claude Code / OpenClaw / 会话。</p>
1821
2249
 
1822
2250
  <!-- 模式切换器 -->
1823
- <div class="segmented-control">
2251
+ <div v-if="!sessionStandalone" class="segmented-control">
1824
2252
  <button :class="['segment', { active: configMode === 'codex' }]" @click="switchConfigMode('codex')">
1825
2253
  Codex 配置
1826
2254
  </button>
@@ -1874,7 +2302,7 @@
1874
2302
  placeholder="例如: gpt-5.3-codex"
1875
2303
  >
1876
2304
  <div class="config-template-hint" v-if="modelsSource === 'unlimited'">
1877
- 当前提供商未提供模型列表,视为不限;模型可手动输入。
2305
+ 当前提供商未提供模型列表,视为不限。模型可手动输入。
1878
2306
  </div>
1879
2307
  <div class="config-template-hint" v-if="modelsSource === 'error'">
1880
2308
  模型列表获取失败,请检查接口或手动输入。
@@ -1927,30 +2355,9 @@
1927
2355
  <div class="selector-header">
1928
2356
  <span class="selector-title">配置健康检查</span>
1929
2357
  </div>
1930
- <div class="config-template-hint">
1931
- 检测 base_url、API Key 与模型可用性(可选远程探测会发起真实请求,可能产生费用)。
1932
- </div>
1933
- <div class="config-template-hint">
1934
- <label class="health-remote-toggle">
1935
- <input type="checkbox" v-model="healthCheckRemote">
1936
- 启用远程探测(请求 base_url 与 /v1/models)
1937
- </label>
1938
- </div>
1939
2358
  <button class="btn-tool" @click="runHealthCheck" :disabled="healthCheckLoading || loading || !!initError">
1940
2359
  {{ healthCheckLoading ? '检查中...' : '运行检查' }}
1941
2360
  </button>
1942
- <div v-if="healthCheckResult" class="health-report">
1943
- <div v-if="healthCheckResult.ok" class="health-ok">未发现问题</div>
1944
- <div v-else>
1945
- <div
1946
- v-for="(issue, index) in healthCheckResult.issues"
1947
- :key="(issue.code || 'issue') + index"
1948
- class="health-issue">
1949
- <div class="health-issue-title">{{ issue.message }}</div>
1950
- <div v-if="issue.suggestion" class="health-issue-suggestion">{{ issue.suggestion }}</div>
1951
- </div>
1952
- </div>
1953
- </div>
1954
2361
  </div>
1955
2362
 
1956
2363
  <div v-if="!loading && !initError" class="card-list">
@@ -2067,7 +2474,7 @@
2067
2474
  添加 OpenClaw 配置
2068
2475
  </button>
2069
2476
  <div class="config-template-hint">
2070
- 默认应用到 <code>~/.openclaw/openclaw.json</code>;支持 JSON5(注释/尾逗号)。
2477
+ 默认应用到 <code>~/.openclaw/openclaw.json</code>。支持 JSON5(注释/尾逗号)。
2071
2478
  </div>
2072
2479
 
2073
2480
  <div class="selector-section">
@@ -2082,6 +2489,22 @@
2082
2489
  </button>
2083
2490
  </div>
2084
2491
 
2492
+ <div class="selector-section">
2493
+ <div class="selector-header">
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
+
2085
2508
  <div class="card-list">
2086
2509
  <div v-for="(config, name) in openclawConfigs" :key="name"
2087
2510
  :class="['card', { active: currentOpenclawConfig === name }]"
@@ -2116,12 +2539,29 @@
2116
2539
  </div>
2117
2540
  </div>
2118
2541
 
2119
- <!-- 会话浏览模式 -->
2120
- <div v-show="configMode === 'sessions'" class="mode-content">
2121
- <div class="selector-section">
2122
- <div class="selector-header">
2123
- <span class="selector-title">会话来源</span>
2124
- </div>
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>
2125
2565
  <div class="session-toolbar">
2126
2566
  <div class="session-toolbar-group">
2127
2567
  <select class="session-source-select" v-model="sessionFilterSource" @change="onSessionSourceChange" :disabled="sessionsLoading">
@@ -2143,8 +2583,8 @@
2143
2583
  class="session-query-input"
2144
2584
  v-model="sessionQuery"
2145
2585
  @keyup.enter="loadSessions"
2146
- disabled
2147
- placeholder="关键词检索(暂时停用)">
2586
+ :disabled="sessionsLoading || !isSessionQueryEnabled"
2587
+ :placeholder="sessionQueryPlaceholder">
2148
2588
  </div>
2149
2589
  <div class="session-toolbar-group">
2150
2590
  <select
@@ -2178,21 +2618,22 @@
2178
2618
  </div>
2179
2619
  </div>
2180
2620
  <div class="session-hint">
2181
- 关键词/角色/时间筛选暂不可用;<br>
2621
+ 关键词检索仅 Codex 可用;<br>
2622
+ 角色/时间筛选暂不可用;<br>
2182
2623
  仅支持来源与路径筛选,右侧仅查看/导出。
2183
2624
  </div>
2184
2625
  </div>
2185
2626
 
2186
- <div v-if="sessionsLoading" class="state-message">
2627
+ <div v-if="!sessionStandalone && sessionsLoading" class="state-message">
2187
2628
  会话加载中...
2188
2629
  </div>
2189
2630
 
2190
- <div v-else-if="sessionsList.length === 0" class="session-empty">
2631
+ <div v-else-if="!sessionStandalone && sessionsList.length === 0" class="session-empty">
2191
2632
  暂无可用会话记录
2192
2633
  </div>
2193
2634
 
2194
- <div v-else class="session-layout">
2195
- <div class="session-list">
2635
+ <div v-else :class="['session-layout', { 'session-standalone': sessionStandalone }]">
2636
+ <div v-if="!sessionStandalone" class="session-list">
2196
2637
  <div
2197
2638
  v-for="session in sessionsList"
2198
2639
  :key="session.source + '-' + session.sessionId + '-' + session.filePath"
@@ -2220,6 +2661,21 @@
2220
2661
  <path d="M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2"></path>
2221
2662
  </svg>
2222
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>
2223
2679
  </div>
2224
2680
  </div>
2225
2681
  <div class="session-item-meta">
@@ -2248,16 +2704,37 @@
2248
2704
  <span class="session-preview-meta-item">{{ activeSession.cwd }}</span>
2249
2705
  </div>
2250
2706
  </div>
2251
- <div class="session-actions">
2707
+ <div v-if="!sessionStandalone" class="session-actions">
2252
2708
  <button class="btn-session-refresh" @click="loadActiveSessionDetail" :disabled="sessionDetailLoading || !activeSession">
2253
2709
  {{ sessionDetailLoading ? '加载中...' : '刷新内容' }}
2254
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>
2255
2725
  <button
2256
2726
  class="btn-session-export"
2257
2727
  @click="exportSession(activeSession)"
2258
2728
  :disabled="!activeSession || sessionExporting[getSessionExportKey(activeSession)]">
2259
2729
  {{ (activeSession && sessionExporting[getSessionExportKey(activeSession)]) ? '导出中...' : '导出记录' }}
2260
2730
  </button>
2731
+ <button
2732
+ v-if="!sessionStandalone"
2733
+ class="btn-session-open"
2734
+ @click="openSessionStandalone(activeSession)"
2735
+ :disabled="!activeSession">
2736
+ 新页查看
2737
+ </button>
2261
2738
  </div>
2262
2739
  </div>
2263
2740
 
@@ -2293,12 +2770,14 @@
2293
2770
  </div>
2294
2771
  </template>
2295
2772
 
2296
- <div v-else class="session-preview-empty">
2297
- 请先在左侧选择一个会话
2298
- </div>
2299
- </div>
2300
- </div>
2301
- </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>
2302
2781
 
2303
2782
  <!-- 加载状态 -->
2304
2783
  <div v-if="loading" class="state-message">
@@ -2471,6 +2950,233 @@
2471
2950
  </div>
2472
2951
  </div>
2473
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
+
2474
3180
  <div class="form-group">
2475
3181
  <label class="form-label">OpenClaw 配置(JSON5)</label>
2476
3182
  <textarea
@@ -2479,7 +3185,7 @@
2479
3185
  spellcheck="false"
2480
3186
  placeholder="在这里编辑 OpenClaw 配置(JSON5)"></textarea>
2481
3187
  <div class="template-editor-warning">
2482
- 保存仅写入本地配置库;点击“保存并应用”后会写入 openclaw.json。
3188
+ 保存仅写入本地配置库。点击“保存并应用”后会写入 openclaw.json。
2483
3189
  </div>
2484
3190
  </div>
2485
3191
 
@@ -2507,7 +3213,7 @@
2507
3213
  spellcheck="false"
2508
3214
  placeholder="在这里编辑 config.toml 模板内容"></textarea>
2509
3215
  <div class="template-editor-warning">
2510
- 工具不会自动改动 `config.toml`;只有点击“确认应用模板”后才写入。
3216
+ 工具不会自动改动 `config.toml`。只有点击“确认应用模板”后才写入。
2511
3217
  </div>
2512
3218
  </div>
2513
3219
 
@@ -2522,7 +3228,15 @@
2522
3228
 
2523
3229
  <div v-if="showAgentsModal" class="modal-overlay" @click.self="closeAgentsModal">
2524
3230
  <div class="modal modal-wide">
2525
- <div class="modal-title">{{ agentsModalTitle }}</div>
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>
2526
3240
 
2527
3241
  <div class="form-group">
2528
3242
  <label class="form-label">目标文件</label>
@@ -2646,13 +3360,22 @@
2646
3360
  },
2647
3361
  sessionPathRequestSeq: 0,
2648
3362
  sessionExporting: {},
3363
+ sessionCloning: {},
3364
+ sessionDeleting: {},
2649
3365
  activeSession: null,
2650
3366
  activeSessionMessages: [],
2651
3367
  activeSessionDetailError: '',
2652
3368
  activeSessionDetailClipped: false,
2653
- sessionDetailLoading: false,
2654
- sessionDetailRequestSeq: 0,
2655
- 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: {},
2656
3379
  speedLoading: {},
2657
3380
  newProvider: { name: '', url: '', key: '' },
2658
3381
  editingProvider: { name: '', url: '', key: '' },
@@ -2688,6 +3411,37 @@
2688
3411
  openclawFileLoading: false,
2689
3412
  openclawSaving: false,
2690
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: [],
2691
3445
  recentConfigs: [],
2692
3446
  recentLoading: false,
2693
3447
  healthCheckLoading: false,
@@ -2696,6 +3450,7 @@
2696
3450
  }
2697
3451
  },
2698
3452
  mounted() {
3453
+ this.initSessionStandalone();
2699
3454
  const savedConfigs = localStorage.getItem('claudeConfigs');
2700
3455
  if (savedConfigs) {
2701
3456
  try {
@@ -2742,6 +3497,15 @@
2742
3497
  }
2743
3498
  this.loadAll();
2744
3499
  },
3500
+
3501
+ computed: {
3502
+ isSessionQueryEnabled() {
3503
+ return this.sessionFilterSource === 'codex';
3504
+ },
3505
+ sessionQueryPlaceholder() {
3506
+ return this.isSessionQueryEnabled ? '关键词检索' : '仅 Codex 支持关键词检索';
3507
+ }
3508
+ },
2745
3509
  methods: {
2746
3510
  async loadAll() {
2747
3511
  this.loading = true;
@@ -2758,7 +3522,7 @@
2758
3522
  this.providersList = listRes.providers;
2759
3523
  await this.loadModelsForProvider(this.currentProvider);
2760
3524
  if (statusRes.configReady === false) {
2761
- this.showMessage(statusRes.configNotice || '未检测到 config.toml,已加载默认模板;请在模板编辑器确认后创建。', 'info');
3525
+ this.showMessage(statusRes.configNotice || '未检测到 config.toml,已加载默认模板。请在模板编辑器确认后创建。', 'info');
2762
3526
  }
2763
3527
  if (statusRes.initNotice) {
2764
3528
  this.showMessage(statusRes.initNotice, 'info');
@@ -2901,6 +3665,100 @@
2901
3665
  }
2902
3666
  },
2903
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
+
2904
3762
  getSessionExportKey(session) {
2905
3763
  return `${session.source || 'unknown'}:${session.sessionId || ''}:${session.filePath || ''}`;
2906
3764
  },
@@ -2912,6 +3770,23 @@
2912
3770
  return source === 'codex' && !!sessionId;
2913
3771
  },
2914
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
+
2915
3790
  buildResumeCommand(session) {
2916
3791
  const sessionId = session && session.sessionId ? String(session.sessionId).trim() : '';
2917
3792
  return `codex resume ${this.quoteResumeArg(sessionId)}`;
@@ -2948,6 +3823,20 @@
2948
3823
  }
2949
3824
  },
2950
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
+
2951
3840
  async copyResumeCommand(session) {
2952
3841
  if (!this.isResumeCommandAvailable(session)) {
2953
3842
  this.showMessage('当前会话不支持生成恢复命令', 'error');
@@ -2971,6 +3860,71 @@
2971
3860
  this.showMessage('复制失败,请手动复制命令', 'error');
2972
3861
  },
2973
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
+
2974
3928
  normalizeSessionPathValue(value) {
2975
3929
  if (typeof value !== 'string') return '';
2976
3930
  return value.trim();
@@ -3134,7 +4088,7 @@
3134
4088
  if (this.sessionsLoading) return;
3135
4089
  this.sessionsLoading = true;
3136
4090
  this.activeSessionDetailError = '';
3137
- const query = this.sessionQuery;
4091
+ const query = this.isSessionQueryEnabled ? this.sessionQuery : '';
3138
4092
  try {
3139
4093
  const res = await api('list-sessions', {
3140
4094
  source: this.sessionFilterSource,
@@ -3142,7 +4096,7 @@
3142
4096
  query,
3143
4097
  queryMode: 'and',
3144
4098
  queryScope: 'content',
3145
- contentScanLimit: 2,
4099
+ contentScanLimit: 50,
3146
4100
  roleFilter: this.sessionRoleFilter,
3147
4101
  timeRangePreset: this.sessionTimePreset,
3148
4102
  limit: 200,
@@ -3184,21 +4138,66 @@
3184
4138
  }
3185
4139
  },
3186
4140
 
3187
- async selectSession(session) {
3188
- if (!session) return;
3189
- if (this.activeSession && this.getSessionExportKey(this.activeSession) === this.getSessionExportKey(session)) return;
3190
- this.activeSession = session;
3191
- this.activeSessionMessages = [];
3192
- this.activeSessionDetailError = '';
3193
- this.activeSessionDetailClipped = false;
3194
- await this.loadActiveSessionDetail();
3195
- },
3196
-
3197
- async loadActiveSessionDetail() {
3198
- if (!this.activeSession) {
3199
- this.activeSessionMessages = [];
3200
- this.activeSessionDetailError = '';
3201
- 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;
3202
4201
  return;
3203
4202
  }
3204
4203
 
@@ -3226,6 +4225,20 @@
3226
4225
 
3227
4226
  this.activeSessionMessages = Array.isArray(res.messages) ? res.messages : [];
3228
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
+ }
3229
4242
  if (res.updatedAt) {
3230
4243
  this.activeSession.updatedAt = res.updatedAt;
3231
4244
  }
@@ -3270,17 +4283,22 @@
3270
4283
  sessionId: session.sessionId,
3271
4284
  filePath: session.filePath
3272
4285
  });
3273
- if (res.error) {
3274
- this.showMessage(res.error, 'error');
3275
- return;
3276
- }
3277
-
3278
- const fileName = res.fileName || `${session.source || 'session'}-${session.sessionId || Date.now()}.md`;
3279
- this.downloadTextFile(fileName, res.content || '');
3280
- this.showMessage('会话导出完成', 'success');
3281
- } catch (e) {
3282
- this.showMessage('导出失败: ' + e.message, 'error');
3283
- } 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 {
3284
4302
  this.sessionExporting[key] = false;
3285
4303
  }
3286
4304
  },
@@ -3328,14 +4346,44 @@
3328
4346
  this.healthCheckResult = null;
3329
4347
  try {
3330
4348
  const res = await api('config-health-check', {
3331
- remote: this.healthCheckRemote
4349
+ remote: false
3332
4350
  });
3333
4351
  if (res && typeof res === 'object') {
3334
- this.healthCheckResult = res;
3335
- if (res.ok) {
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) {
3336
4386
  this.showMessage('健康检查通过', 'success');
3337
- } else {
3338
- this.showMessage('发现配置问题,请查看详情', 'error');
3339
4387
  }
3340
4388
  } else {
3341
4389
  this.healthCheckResult = null;
@@ -3456,7 +4504,44 @@
3456
4504
  }
3457
4505
  },
3458
4506
 
3459
- setAgentsModalContext(context) {
4507
+ async openOpenclawWorkspaceEditor() {
4508
+ const fileName = (this.openclawWorkspaceFileName || '').trim();
4509
+ if (!fileName) {
4510
+ this.showMessage('请输入工作区文件名', 'error');
4511
+ return;
4512
+ }
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
+ }
3460
4545
  this.agentsContext = context === 'openclaw' ? 'openclaw' : 'codex';
3461
4546
  if (this.agentsContext === 'openclaw') {
3462
4547
  this.agentsModalTitle = 'OpenClaw AGENTS.md 编辑器';
@@ -3465,6 +4550,7 @@
3465
4550
  this.agentsModalTitle = 'AGENTS.md 编辑器';
3466
4551
  this.agentsModalHint = '保存后会写入目标 AGENTS.md(与 config.toml 同级)。';
3467
4552
  }
4553
+ this.agentsWorkspaceFileName = '';
3468
4554
  },
3469
4555
 
3470
4556
  closeAgentsModal() {
@@ -3474,27 +4560,36 @@
3474
4560
  this.agentsExists = false;
3475
4561
  this.agentsLineEnding = '\n';
3476
4562
  this.agentsSaving = false;
4563
+ this.agentsWorkspaceFileName = '';
3477
4564
  this.setAgentsModalContext('codex');
3478
4565
  },
3479
4566
 
3480
4567
  async applyAgentsContent() {
3481
4568
  this.agentsSaving = true;
3482
4569
  try {
3483
- const action = this.agentsContext === 'openclaw'
3484
- ? 'apply-openclaw-agents-file'
3485
- : 'apply-agents-file';
3486
- const res = await api(action, {
4570
+ let action = 'apply-agents-file';
4571
+ const params = {
3487
4572
  content: this.agentsContent,
3488
4573
  lineEnding: this.agentsLineEnding
3489
- });
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);
3490
4582
  if (res.error) {
3491
4583
  this.showMessage(res.error, 'error');
3492
4584
  return;
3493
4585
  }
3494
- this.showMessage('AGENTS.md 已保存', 'success');
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');
3495
4590
  this.closeAgentsModal();
3496
4591
  } catch (e) {
3497
- this.showMessage('保存 AGENTS.md 失败: ' + e.message, 'error');
4592
+ this.showMessage('保存文件失败: ' + e.message, 'error');
3498
4593
  } finally {
3499
4594
  this.agentsSaving = false;
3500
4595
  }
@@ -3780,6 +4875,763 @@
3780
4875
  };
3781
4876
  },
3782
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
+
3783
5635
  openclawHasContent(config) {
3784
5636
  return !!(config && typeof config.content === 'string' && config.content.trim());
3785
5637
  },
@@ -3800,23 +5652,24 @@
3800
5652
  this.openclawEditorTitle = '添加 OpenClaw 配置';
3801
5653
  this.openclawEditing = {
3802
5654
  name: '',
3803
- content: DEFAULT_OPENCLAW_TEMPLATE,
5655
+ content: '',
3804
5656
  lockName: false
3805
5657
  };
3806
5658
  this.openclawConfigPath = '';
3807
5659
  this.openclawConfigExists = false;
3808
5660
  this.openclawLineEnding = '\n';
5661
+ void this.loadOpenclawConfigFromFile({ silent: true, force: true, fallbackToTemplate: true });
3809
5662
  this.showOpenclawConfigModal = true;
3810
5663
  },
3811
5664
 
3812
5665
  openOpenclawEditModal(name) {
3813
- const config = this.openclawConfigs[name];
3814
5666
  this.openclawEditorTitle = `编辑 OpenClaw 配置: ${name}`;
3815
5667
  this.openclawEditing = {
3816
5668
  name,
3817
- content: (config && config.content) ? config.content : '',
5669
+ content: '',
3818
5670
  lockName: true
3819
5671
  };
5672
+ void this.loadOpenclawConfigFromFile({ silent: true, force: true, fallbackToTemplate: true });
3820
5673
  this.showOpenclawConfigModal = true;
3821
5674
  },
3822
5675
 
@@ -3825,27 +5678,41 @@
3825
5678
  this.openclawEditing = { name: '', content: '', lockName: false };
3826
5679
  this.openclawSaving = false;
3827
5680
  this.openclawApplying = false;
5681
+ this.resetOpenclawStructured();
5682
+ this.resetOpenclawQuick();
3828
5683
  },
3829
5684
 
3830
- async loadOpenclawConfigFromFile() {
5685
+ async loadOpenclawConfigFromFile(options = {}) {
5686
+ const silent = !!options.silent;
5687
+ const force = !!options.force;
5688
+ const fallbackToTemplate = options.fallbackToTemplate !== false;
3831
5689
  this.openclawFileLoading = true;
3832
5690
  try {
3833
5691
  const res = await api('get-openclaw-config');
3834
5692
  if (res.error) {
3835
- this.showMessage(res.error, 'error');
5693
+ if (!silent) {
5694
+ this.showMessage(res.error, 'error');
5695
+ }
3836
5696
  return;
3837
5697
  }
3838
5698
  this.openclawConfigPath = res.path || '';
3839
5699
  this.openclawConfigExists = !!res.exists;
3840
5700
  this.openclawLineEnding = res.lineEnding === '\r\n' ? '\r\n' : '\n';
3841
- if (res.content && res.content.trim()) {
5701
+ const hasContent = !!(res.content && res.content.trim());
5702
+ const shouldOverride = force || !this.openclawEditing.content || !this.openclawEditing.content.trim();
5703
+ if (hasContent && shouldOverride) {
3842
5704
  this.openclawEditing.content = res.content;
3843
- } else if (!this.openclawEditing.content) {
5705
+ } else if (!hasContent && shouldOverride && fallbackToTemplate) {
3844
5706
  this.openclawEditing.content = DEFAULT_OPENCLAW_TEMPLATE;
3845
5707
  }
3846
- this.showMessage('已加载当前 OpenClaw 配置', 'success');
5708
+ this.syncOpenclawStructuredFromText({ silent: true });
5709
+ if (!silent) {
5710
+ this.showMessage('已加载当前 OpenClaw 配置', 'success');
5711
+ }
3847
5712
  } catch (e) {
3848
- this.showMessage('加载 OpenClaw 配置失败: ' + e.message, 'error');
5713
+ if (!silent) {
5714
+ this.showMessage('加载 OpenClaw 配置失败: ' + e.message, 'error');
5715
+ }
3849
5716
  } finally {
3850
5717
  this.openclawFileLoading = false;
3851
5718
  }
@@ -3954,19 +5821,93 @@
3954
5821
  return `${ms}ms`;
3955
5822
  },
3956
5823
 
3957
- async runSpeedTest(name) {
3958
- 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;
3959
5885
  this.speedLoading[name] = true;
3960
- const res = await api('speed-test', { name });
3961
- if (res.error) {
3962
- this.speedResults[name] = { ok: false, error: res.error };
3963
- this.showMessage(res.error, 'error');
3964
- } 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
+ }
3965
5895
  this.speedResults[name] = res;
3966
- const status = res.status ? ` (${res.status})` : '';
3967
- 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;
3968
5910
  }
3969
- this.speedLoading[name] = false;
3970
5911
  },
3971
5912
 
3972
5913
  showMessage(text, type) {