codexmate 0.0.8 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/web-ui.html CHANGED
@@ -4,8 +4,8 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Codex Mate</title>
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>
7
+ <script src="res/vue.global.js"></script>
8
+ <script src="res/json5.min.js"></script>
9
9
  <style>
10
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');
11
11
 
@@ -168,25 +168,113 @@
168
168
  ============================================ */
169
169
  .container {
170
170
  width: 100%;
171
- max-width: 1200px;
171
+ max-width: 1280px;
172
172
  margin: 0 auto;
173
- padding: var(--spacing-md) var(--spacing-sm) var(--spacing-lg);
173
+ padding: 16px 12px 28px;
174
174
  position: relative;
175
175
  z-index: 1;
176
176
  }
177
177
 
178
178
  /* ============================================
179
- 布局:单列居中
179
+ 布局:双列(侧栏 + 主区)
180
180
  ============================================ */
181
181
  .app-shell {
182
182
  display: grid;
183
- grid-template-columns: 1fr;
183
+ grid-template-columns: 320px minmax(0, 1fr);
184
184
  gap: var(--spacing-md);
185
185
  align-items: flex-start;
186
186
  }
187
187
 
188
- .side-nav {
189
- display: none;
188
+ .app-shell.standalone {
189
+ grid-template-columns: 1fr;
190
+ }
191
+
192
+ .side-rail {
193
+ position: sticky;
194
+ top: var(--spacing-md);
195
+ align-self: start;
196
+ display: flex;
197
+ flex-direction: column;
198
+ gap: var(--spacing-sm);
199
+ padding: var(--spacing-md) var(--spacing-sm);
200
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.95) 0%, rgba(255, 250, 245, 0.9) 100%);
201
+ border: 1px solid rgba(216, 201, 184, 0.65);
202
+ border-radius: var(--radius-xl);
203
+ box-shadow: var(--shadow-card);
204
+ min-height: 420px;
205
+ }
206
+
207
+ .side-rail .brand-title {
208
+ font-size: 24px;
209
+ margin-bottom: 2px;
210
+ }
211
+
212
+ .side-section {
213
+ display: flex;
214
+ flex-direction: column;
215
+ gap: 10px;
216
+ }
217
+
218
+ .side-section-title {
219
+ font-size: var(--font-size-secondary);
220
+ font-weight: var(--font-weight-secondary);
221
+ color: var(--color-text-tertiary);
222
+ letter-spacing: 0.01em;
223
+ padding: 0 var(--spacing-xs);
224
+ }
225
+
226
+ .side-item {
227
+ width: 100%;
228
+ text-align: left;
229
+ padding: 12px var(--spacing-sm);
230
+ border-radius: var(--radius-lg);
231
+ border: 1px solid var(--color-border-soft);
232
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, rgba(255, 247, 240, 0.95) 100%);
233
+ color: var(--color-text-secondary);
234
+ cursor: pointer;
235
+ transition: all var(--transition-normal) var(--ease-spring);
236
+ display: flex;
237
+ flex-direction: column;
238
+ gap: 6px;
239
+ box-shadow: var(--shadow-subtle);
240
+ }
241
+
242
+ .side-item:hover {
243
+ border-color: var(--color-brand);
244
+ color: var(--color-text-primary);
245
+ transform: translateY(-1px);
246
+ box-shadow: var(--shadow-card-hover);
247
+ }
248
+
249
+ .side-item.active {
250
+ border-color: var(--color-brand);
251
+ background: linear-gradient(135deg, rgba(201, 94, 75, 0.14), rgba(255, 255, 255, 0.96));
252
+ color: var(--color-text-primary);
253
+ box-shadow: var(--shadow-float);
254
+ }
255
+
256
+ .side-item-title {
257
+ font-size: var(--font-size-body);
258
+ font-weight: var(--font-weight-secondary);
259
+ letter-spacing: -0.01em;
260
+ }
261
+
262
+ .side-item-meta {
263
+ font-size: var(--font-size-caption);
264
+ color: var(--color-text-tertiary);
265
+ display: flex;
266
+ gap: 8px;
267
+ flex-wrap: wrap;
268
+ }
269
+
270
+ .side-item-meta > span {
271
+ min-width: 0;
272
+ overflow-wrap: anywhere;
273
+ word-break: break-word;
274
+ }
275
+
276
+ .top-tabs {
277
+ display: none !important;
190
278
  }
191
279
 
192
280
  .brand-block {
@@ -245,6 +333,40 @@
245
333
  background: linear-gradient(135deg, rgba(201, 94, 75, 0.12), rgba(255, 255, 255, 0.95));
246
334
  }
247
335
 
336
+ .status-strip {
337
+ display: flex;
338
+ flex-wrap: wrap;
339
+ gap: var(--spacing-xs);
340
+ margin-bottom: var(--spacing-sm);
341
+ margin-top: 6px;
342
+ }
343
+
344
+ .status-chip {
345
+ min-width: 200px;
346
+ padding: 10px 12px;
347
+ border-radius: var(--radius-lg);
348
+ border: 1px solid var(--color-border-soft);
349
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.98) 0%, rgba(255, 247, 240, 0.96) 100%);
350
+ box-shadow: var(--shadow-subtle);
351
+ }
352
+
353
+ .status-chip .label {
354
+ display: block;
355
+ font-size: var(--font-size-caption);
356
+ color: var(--color-text-tertiary);
357
+ margin-bottom: 4px;
358
+ }
359
+
360
+ .status-chip .value {
361
+ font-size: var(--font-size-body);
362
+ font-weight: var(--font-weight-secondary);
363
+ color: var(--color-text-primary);
364
+ letter-spacing: -0.01em;
365
+ white-space: normal;
366
+ overflow-wrap: anywhere;
367
+ word-break: break-word;
368
+ }
369
+
248
370
  .main-panel {
249
371
  min-width: 0;
250
372
  background: rgba(255, 255, 255, 0.9);
@@ -253,6 +375,9 @@
253
375
  box-shadow: 0 12px 30px rgba(27, 23, 20, 0.08);
254
376
  padding: var(--spacing-md) var(--spacing-lg);
255
377
  backdrop-filter: blur(8px);
378
+ position: relative;
379
+ overflow-x: hidden;
380
+ overflow-y: visible;
256
381
  }
257
382
 
258
383
  .panel-header {
@@ -363,14 +488,14 @@
363
488
  border: 1px solid rgba(255, 255, 255, 0.7);
364
489
  border-radius: var(--radius-lg);
365
490
  box-shadow: var(--shadow-card);
366
- padding: var(--spacing-md);
491
+ padding: 0;
367
492
  }
368
493
 
369
494
  .mode-content {
370
495
  border-radius: var(--radius-lg);
371
496
  background: rgba(255, 255, 255, 0.85);
372
497
  box-shadow: var(--shadow-subtle);
373
- padding: var(--spacing-sm);
498
+ padding: 10px;
374
499
  }
375
500
 
376
501
  /* ============================================
@@ -462,7 +587,7 @@
462
587
  .card {
463
588
  background: linear-gradient(180deg, #fffdf9 0%, #fff8f2 100%);
464
589
  border-radius: var(--radius-lg);
465
- padding: var(--spacing-sm);
590
+ padding: 10px;
466
591
  display: flex;
467
592
  align-items: center;
468
593
  justify-content: space-between;
@@ -515,9 +640,9 @@
515
640
  }
516
641
 
517
642
  .card.active {
518
- background: linear-gradient(to bottom, rgba(210, 107, 90, 0.12) 0%, rgba(255, 255, 255, 0.98) 100%);
519
- border-color: rgba(201, 94, 75, 0.45);
520
- box-shadow: var(--shadow-float);
643
+ background: linear-gradient(to bottom, rgba(210, 107, 90, 0.14) 0%, rgba(255, 255, 255, 0.98) 100%);
644
+ border-color: rgba(201, 94, 75, 0.55);
645
+ box-shadow: 0 10px 28px rgba(210, 107, 90, 0.14);
521
646
  }
522
647
 
523
648
  .card.active::before {
@@ -597,7 +722,7 @@
597
722
  /* 卡片操作按钮 - hover 显示 */
598
723
  .card-actions {
599
724
  display: flex;
600
- gap: 4px;
725
+ gap: 8px;
601
726
  opacity: 0;
602
727
  transform: translateX(4px);
603
728
  transition: all var(--transition-normal) var(--ease-spring);
@@ -614,23 +739,24 @@
614
739
  }
615
740
 
616
741
  .card-action-btn {
617
- width: 28px;
618
- height: 28px;
619
- border-radius: var(--radius-sm);
620
- border: none;
621
- background: transparent;
622
- color: var(--color-text-tertiary);
742
+ width: 40px;
743
+ height: 40px;
744
+ border-radius: 10px;
745
+ border: 1px solid rgba(70, 86, 110, 0.22);
746
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(255, 255, 255, 0.9));
747
+ color: var(--color-text-secondary);
623
748
  cursor: pointer;
624
749
  display: flex;
625
750
  align-items: center;
626
751
  justify-content: center;
627
752
  transition: all var(--transition-fast) var(--ease-spring);
753
+ box-shadow: inset 0 1px 2px rgba(31, 26, 23, 0.04);
628
754
  }
629
755
 
630
756
  .card-action-btn:hover {
631
- background: linear-gradient(135deg, var(--color-bg) 0%, rgba(247, 241, 232, 0.8) 100%);
632
- color: var(--color-text-secondary);
633
- transform: scale(1.08);
757
+ background: linear-gradient(135deg, rgba(210, 107, 90, 0.08) 0%, rgba(255, 255, 255, 0.95) 100%);
758
+ color: var(--color-text-primary);
759
+ transform: translateY(-1px);
634
760
  }
635
761
 
636
762
  .card-action-btn.delete:hover {
@@ -639,8 +765,8 @@
639
765
  }
640
766
 
641
767
  .card-action-btn svg {
642
- width: 16px;
643
- height: 16px;
768
+ width: 18px;
769
+ height: 18px;
644
770
  }
645
771
 
646
772
  /* ============================================
@@ -1094,11 +1220,16 @@
1094
1220
  }
1095
1221
 
1096
1222
  .session-count-pill {
1223
+ display: inline-flex;
1224
+ align-items: center;
1225
+ justify-content: center;
1097
1226
  font-size: var(--font-size-caption);
1098
1227
  color: var(--color-text-secondary);
1099
1228
  border: 1px solid var(--color-border-soft);
1100
1229
  border-radius: 999px;
1101
- padding: 2px 8px;
1230
+ padding: 0 8px;
1231
+ height: 22px;
1232
+ line-height: 1;
1102
1233
  background: rgba(247, 241, 232, 0.7);
1103
1234
  white-space: nowrap;
1104
1235
  margin-left: auto;
@@ -1209,14 +1340,14 @@
1209
1340
  box-shadow: inset 0 0 0 6px rgba(255, 255, 255, 0.7);
1210
1341
  }
1211
1342
 
1212
- .session-layout {
1213
- display: grid;
1214
- grid-template-columns: minmax(280px, 360px) minmax(0, 1fr);
1215
- gap: var(--spacing-sm);
1216
- align-items: start;
1217
- height: min(72vh, 760px);
1218
- min-height: 520px;
1219
- }
1343
+ .session-layout {
1344
+ display: grid;
1345
+ grid-template-columns: minmax(215px, 295px) minmax(0, 1fr);
1346
+ gap: var(--spacing-sm);
1347
+ align-items: start;
1348
+ height: min(72vh, 760px);
1349
+ min-height: 520px;
1350
+ }
1220
1351
 
1221
1352
  .session-layout.session-standalone {
1222
1353
  grid-template-columns: minmax(0, 1fr);
@@ -1289,11 +1420,14 @@
1289
1420
  border: 1px solid var(--color-border-soft);
1290
1421
  border-radius: var(--radius-sm);
1291
1422
  background: linear-gradient(to bottom, var(--color-surface) 0%, rgba(255, 255, 255, 0.92) 100%);
1292
- padding: 10px 12px;
1423
+ padding: 14px 16px;
1293
1424
  cursor: pointer;
1294
1425
  transition: all var(--transition-fast) var(--ease-spring);
1295
1426
  user-select: none;
1296
1427
  min-width: 0;
1428
+ position: relative;
1429
+ overflow: hidden;
1430
+ min-height: 88px;
1297
1431
  }
1298
1432
 
1299
1433
  .session-item-header {
@@ -1318,6 +1452,18 @@
1318
1452
  transform: translateY(-1px);
1319
1453
  }
1320
1454
 
1455
+ .session-item::before {
1456
+ content: "";
1457
+ position: absolute;
1458
+ left: 0;
1459
+ top: 10px;
1460
+ bottom: 10px;
1461
+ width: 3px;
1462
+ border-radius: 999px;
1463
+ background: rgba(210, 107, 90, 0.15);
1464
+ transition: background var(--transition-fast) var(--ease-spring);
1465
+ }
1466
+
1321
1467
  .session-item:active {
1322
1468
  transform: scale(0.99);
1323
1469
  }
@@ -1328,19 +1474,23 @@
1328
1474
  box-shadow: 0 6px 16px rgba(210, 107, 90, 0.12);
1329
1475
  }
1330
1476
 
1331
- .session-item-title {
1332
- font-size: var(--font-size-body);
1333
- font-weight: var(--font-weight-secondary);
1334
- color: var(--color-text-primary);
1335
- line-height: 1.35;
1336
- display: -webkit-box;
1337
- -webkit-line-clamp: 2;
1338
- -webkit-box-orient: vertical;
1339
- overflow: hidden;
1340
- word-break: break-word;
1341
- flex: 1;
1477
+ .session-item.active::before {
1478
+ background: linear-gradient(180deg, rgba(201, 94, 75, 0.9), rgba(201, 94, 75, 0.4));
1342
1479
  }
1343
1480
 
1481
+ .session-item-title {
1482
+ font-size: var(--font-size-body);
1483
+ font-weight: var(--font-weight-secondary);
1484
+ color: var(--color-text-primary);
1485
+ line-height: 1.5;
1486
+ display: block;
1487
+ white-space: nowrap;
1488
+ overflow: hidden;
1489
+ text-overflow: ellipsis;
1490
+ flex: 1;
1491
+ max-width: 75%;
1492
+ }
1493
+
1344
1494
  .session-item-actions {
1345
1495
  display: inline-flex;
1346
1496
  align-items: center;
@@ -1409,26 +1559,37 @@
1409
1559
  white-space: nowrap;
1410
1560
  }
1411
1561
 
1412
- .session-item-snippet {
1413
- color: var(--color-text-secondary);
1414
- white-space: normal;
1415
- background: rgba(210, 107, 90, 0.08);
1416
- border-radius: 8px;
1417
- padding: 6px 8px;
1418
- border: 1px solid rgba(210, 107, 90, 0.15);
1419
- }
1420
-
1421
- .session-preview {
1422
- border: 1px solid var(--color-border-soft);
1423
- border-radius: var(--radius-xl);
1424
- background: linear-gradient(to bottom, var(--color-surface-elevated) 0%, rgba(255, 255, 255, 0.96) 100%);
1425
- box-shadow: var(--shadow-card);
1562
+ .session-item-snippet {
1563
+ color: var(--color-text-secondary);
1564
+ white-space: nowrap;
1565
+ overflow: hidden;
1566
+ text-overflow: ellipsis;
1567
+ display: block;
1568
+ max-width: 100%;
1569
+ background: rgba(210, 107, 90, 0.08);
1570
+ border-radius: 8px;
1571
+ padding: 6px 8px;
1572
+ border: 1px solid rgba(210, 107, 90, 0.15);
1573
+ }
1574
+
1575
+ .session-preview {
1576
+ border: 1px solid var(--color-border-soft);
1577
+ border-radius: var(--radius-xl);
1578
+ background: linear-gradient(to bottom, var(--color-surface-elevated) 0%, rgba(255, 255, 255, 0.96) 100%);
1579
+ box-shadow: var(--shadow-card);
1426
1580
  min-height: 0;
1427
1581
  max-height: none;
1428
1582
  height: 100%;
1429
1583
  display: flex;
1430
1584
  flex-direction: column;
1431
1585
  overflow: hidden;
1586
+ transform: translateX(4px);
1587
+ transition: transform var(--transition-normal) var(--ease-spring-soft), box-shadow var(--transition-normal) var(--ease-spring-soft);
1588
+ }
1589
+
1590
+ .session-preview.active {
1591
+ box-shadow: var(--shadow-float);
1592
+ transform: translateX(0);
1432
1593
  }
1433
1594
 
1434
1595
  .session-preview-scroll {
@@ -1506,15 +1667,16 @@
1506
1667
  opacity: 0.7;
1507
1668
  }
1508
1669
 
1509
- .session-actions {
1510
- display: flex;
1511
- align-items: center;
1512
- gap: 8px;
1513
- flex-shrink: 0;
1514
- margin-left: 0;
1515
- flex-wrap: wrap;
1516
- justify-content: flex-end;
1517
- }
1670
+ .session-actions {
1671
+ display: flex;
1672
+ align-items: center;
1673
+ gap: 8px;
1674
+ flex: 0 1 auto;
1675
+ max-width: 100%;
1676
+ margin-left: 0;
1677
+ flex-wrap: wrap;
1678
+ justify-content: flex-end;
1679
+ }
1518
1680
 
1519
1681
  .session-preview-body {
1520
1682
  flex: 1;
@@ -1618,7 +1780,7 @@
1618
1780
  box-shadow: inset 0 0 0 6px rgba(255, 255, 255, 0.7);
1619
1781
  }
1620
1782
 
1621
- @media (max-width: 900px) {
1783
+ @media (max-width: 1100px) {
1622
1784
  .session-layout {
1623
1785
  grid-template-columns: 1fr;
1624
1786
  height: auto;
@@ -1647,6 +1809,9 @@
1647
1809
  min-height: 360px;
1648
1810
  max-height: none;
1649
1811
  height: auto;
1812
+ position: relative;
1813
+ transform: none;
1814
+ box-shadow: var(--shadow-card);
1650
1815
  }
1651
1816
 
1652
1817
  .session-preview-header {
@@ -1657,6 +1822,10 @@
1657
1822
  .session-actions {
1658
1823
  justify-content: flex-start;
1659
1824
  }
1825
+
1826
+ .session-preview.active {
1827
+ box-shadow: var(--shadow-float);
1828
+ }
1660
1829
  }
1661
1830
 
1662
1831
  @media (max-width: 520px) {
@@ -2350,15 +2519,30 @@
2350
2519
 
2351
2520
  @media (max-width: 960px) {
2352
2521
  .container {
2353
- padding: var(--spacing-sm);
2522
+ padding: 12px;
2523
+ }
2524
+ .app-shell {
2525
+ grid-template-columns: 1fr;
2526
+ }
2527
+ .side-rail {
2528
+ display: none;
2354
2529
  }
2355
2530
  .main-panel {
2356
2531
  padding: var(--spacing-sm) var(--spacing-sm);
2357
2532
  border-radius: 14px;
2358
2533
  }
2359
2534
  .top-tabs {
2535
+ display: grid !important;
2360
2536
  grid-template-columns: repeat(2, minmax(0, 1fr));
2361
2537
  }
2538
+ .status-strip {
2539
+ gap: var(--spacing-sm);
2540
+ margin-top: 4px;
2541
+ }
2542
+ .status-chip {
2543
+ flex: 1 1 calc(50% - var(--spacing-sm));
2544
+ min-width: 0;
2545
+ }
2362
2546
  }
2363
2547
 
2364
2548
  @media (max-width: 720px) {
@@ -2366,6 +2550,10 @@
2366
2550
  font-size: 40px;
2367
2551
  }
2368
2552
 
2553
+ .hero-title {
2554
+ font-size: 32px;
2555
+ }
2556
+
2369
2557
  .subtitle {
2370
2558
  font-size: var(--font-size-secondary);
2371
2559
  margin-bottom: 16px;
@@ -2375,6 +2563,15 @@
2375
2563
  flex-direction: column;
2376
2564
  gap: 6px;
2377
2565
  }
2566
+
2567
+ .status-strip {
2568
+ flex-direction: row;
2569
+ flex-wrap: wrap;
2570
+ }
2571
+
2572
+ .status-chip {
2573
+ flex: 1 1 100%;
2574
+ }
2378
2575
  }
2379
2576
 
2380
2577
  @media (max-width: 540px) {
@@ -2385,7 +2582,7 @@
2385
2582
  padding: 0 var(--spacing-sm) var(--spacing-md);
2386
2583
  }
2387
2584
  .hero-title {
2388
- font-size: 36px;
2585
+ font-size: 32px;
2389
2586
  }
2390
2587
  .hero-subtitle {
2391
2588
  font-size: var(--font-size-secondary);
@@ -2404,6 +2601,125 @@
2404
2601
  height: auto;
2405
2602
  min-height: 0;
2406
2603
  }
2604
+
2605
+ .status-strip {
2606
+ gap: var(--spacing-xs);
2607
+ }
2608
+
2609
+ .status-chip {
2610
+ flex: 1 1 100%;
2611
+ min-width: 100%;
2612
+ }
2613
+
2614
+ .btn-add,
2615
+ .btn-tool,
2616
+ .card-action-btn,
2617
+ .btn-session-export,
2618
+ .btn-session-open,
2619
+ .btn-session-clone,
2620
+ .btn-session-refresh,
2621
+ .btn-session-delete,
2622
+ .btn-icon,
2623
+ .session-item-copy {
2624
+ min-height: 44px;
2625
+ padding-top: 12px;
2626
+ padding-bottom: 12px;
2627
+ }
2628
+
2629
+ .btn-icon,
2630
+ .session-item-copy {
2631
+ min-width: 44px;
2632
+ }
2633
+
2634
+ .session-item {
2635
+ min-height: 75px;
2636
+ height: 75px;
2637
+ padding: 12px 14px;
2638
+ }
2639
+
2640
+ .session-item-header {
2641
+ flex-direction: row;
2642
+ align-items: center;
2643
+ gap: 8px;
2644
+ }
2645
+
2646
+ .session-item-main {
2647
+ align-items: center;
2648
+ }
2649
+
2650
+ .session-item-copy {
2651
+ width: 20px;
2652
+ height: 20px;
2653
+ min-width: 20px;
2654
+ min-height: 20px;
2655
+ border-radius: 6px;
2656
+ padding: 2px;
2657
+ display: inline-flex;
2658
+ align-items: center;
2659
+ justify-content: center;
2660
+ transform: translate(-3px, 0);
2661
+ }
2662
+
2663
+ .session-item-copy svg {
2664
+ width: 12px;
2665
+ height: 12px;
2666
+ }
2667
+
2668
+ .session-item-title {
2669
+ -webkit-line-clamp: 1;
2670
+ max-height: none;
2671
+ white-space: nowrap;
2672
+ text-overflow: ellipsis;
2673
+ overflow: hidden;
2674
+ }
2675
+
2676
+ .session-item-actions {
2677
+ margin-top: 0;
2678
+ }
2679
+
2680
+ .session-item-meta {
2681
+ margin-top: -2px;
2682
+ margin-bottom: 0;
2683
+ gap: 4px;
2684
+ align-items: center;
2685
+ }
2686
+
2687
+ .session-count-pill {
2688
+ transform: translateY(-6px);
2689
+ }
2690
+
2691
+ .card {
2692
+ padding: 8px;
2693
+ }
2694
+
2695
+ .card-list {
2696
+ gap: 4px;
2697
+ margin-bottom: 4px;
2698
+ }
2699
+
2700
+ .card-actions {
2701
+ gap: 8px;
2702
+ }
2703
+
2704
+ .card-action-btn {
2705
+ width: 40px;
2706
+ height: 40px;
2707
+ border-radius: 10px;
2708
+ }
2709
+
2710
+ .card-action-btn svg {
2711
+ width: 18px;
2712
+ height: 18px;
2713
+ }
2714
+
2715
+ /* 移动端不显示配置状态 pill,节省空间 */
2716
+ .card-trailing .pill {
2717
+ display: none;
2718
+ }
2719
+
2720
+ .session-preview {
2721
+ border-radius: var(--radius-lg);
2722
+ }
2407
2723
  }
2408
2724
  </style>
2409
2725
  </head>
@@ -2418,22 +2734,124 @@
2418
2734
  </div>
2419
2735
  </div>
2420
2736
 
2421
- <div v-if="!sessionStandalone" class="top-tabs">
2737
+ <div v-if="!sessionStandalone" class="top-tabs" role="tablist" aria-label="主导航">
2422
2738
  <button class="top-tab"
2739
+ id="tab-config-codex"
2740
+ role="tab"
2741
+ :tabindex="mainTab === 'config' && configMode === 'codex' ? 0 : -1"
2742
+ :aria-selected="mainTab === 'config' && configMode === 'codex'"
2743
+ :aria-pressed="mainTab === 'config' && configMode === 'codex'"
2744
+ aria-controls="panel-config-codex"
2423
2745
  :class="{ active: mainTab === 'config' && configMode === 'codex' }"
2424
2746
  @click="switchConfigMode('codex')">Codex 配置</button>
2425
2747
  <button class="top-tab"
2748
+ id="tab-config-claude"
2749
+ role="tab"
2750
+ :tabindex="mainTab === 'config' && configMode === 'claude' ? 0 : -1"
2751
+ :aria-selected="mainTab === 'config' && configMode === 'claude'"
2752
+ :aria-pressed="mainTab === 'config' && configMode === 'claude'"
2753
+ aria-controls="panel-config-claude"
2426
2754
  :class="{ active: mainTab === 'config' && configMode === 'claude' }"
2427
2755
  @click="switchConfigMode('claude')">Claude Code 配置</button>
2428
2756
  <button class="top-tab"
2757
+ id="tab-config-openclaw"
2758
+ role="tab"
2759
+ :tabindex="mainTab === 'config' && configMode === 'openclaw' ? 0 : -1"
2760
+ :aria-selected="mainTab === 'config' && configMode === 'openclaw'"
2761
+ :aria-pressed="mainTab === 'config' && configMode === 'openclaw'"
2762
+ aria-controls="panel-config-openclaw"
2429
2763
  :class="{ active: mainTab === 'config' && configMode === 'openclaw' }"
2430
2764
  @click="switchConfigMode('openclaw')">OpenClaw 配置</button>
2431
2765
  <button class="top-tab"
2766
+ id="tab-sessions"
2767
+ role="tab"
2768
+ :tabindex="mainTab === 'sessions' ? 0 : -1"
2769
+ :aria-selected="mainTab === 'sessions'"
2770
+ :aria-pressed="mainTab === 'sessions'"
2771
+ aria-controls="panel-sessions"
2432
2772
  :class="{ active: mainTab === 'sessions' }"
2433
2773
  @click="switchMainTab('sessions')">会话浏览</button>
2434
2774
  </div>
2435
2775
 
2436
2776
  <div :class="['app-shell', { standalone: sessionStandalone }]">
2777
+ <aside class="side-rail" v-if="!sessionStandalone">
2778
+ <div class="brand-block">
2779
+ <div class="brand-title">
2780
+ Codex <span class="accent">Mate</span>
2781
+ </div>
2782
+ <div class="brand-subtitle">
2783
+ 配置 / 会话切换器
2784
+ </div>
2785
+ </div>
2786
+
2787
+ <div class="side-section" role="tablist" aria-label="配置管理">
2788
+ <div class="side-section-title">配置管理</div>
2789
+ <button
2790
+ role="tab"
2791
+ id="side-tab-config-codex"
2792
+ aria-controls="panel-config-codex"
2793
+ :tabindex="mainTab === 'config' && configMode === 'codex' ? 0 : -1"
2794
+ :aria-selected="mainTab === 'config' && configMode === 'codex'"
2795
+ :aria-pressed="mainTab === 'config' && configMode === 'codex'"
2796
+ :class="['side-item', { active: mainTab === 'config' && configMode === 'codex' }]"
2797
+ @click="switchConfigMode('codex')">
2798
+ <div class="side-item-title">Codex 配置</div>
2799
+ <div class="side-item-meta">
2800
+ <span>提供商 / 模型</span>
2801
+ <span v-if="currentProvider">当前 {{ currentProvider }}</span>
2802
+ </div>
2803
+ </button>
2804
+ <button
2805
+ role="tab"
2806
+ id="side-tab-config-claude"
2807
+ aria-controls="panel-config-claude"
2808
+ :tabindex="mainTab === 'config' && configMode === 'claude' ? 0 : -1"
2809
+ :aria-selected="mainTab === 'config' && configMode === 'claude'"
2810
+ :aria-pressed="mainTab === 'config' && configMode === 'claude'"
2811
+ :class="['side-item', { active: mainTab === 'config' && configMode === 'claude' }]"
2812
+ @click="switchConfigMode('claude')">
2813
+ <div class="side-item-title">Claude Code 配置</div>
2814
+ <div class="side-item-meta">
2815
+ <span>Base URL / Key</span>
2816
+ <span v-if="currentClaudeConfig">当前 {{ currentClaudeConfig }}</span>
2817
+ </div>
2818
+ </button>
2819
+ <button
2820
+ role="tab"
2821
+ id="side-tab-config-openclaw"
2822
+ aria-controls="panel-config-openclaw"
2823
+ :tabindex="mainTab === 'config' && configMode === 'openclaw' ? 0 : -1"
2824
+ :aria-selected="mainTab === 'config' && configMode === 'openclaw'"
2825
+ :aria-pressed="mainTab === 'config' && configMode === 'openclaw'"
2826
+ :class="['side-item', { active: mainTab === 'config' && configMode === 'openclaw' }]"
2827
+ @click="switchConfigMode('openclaw')">
2828
+ <div class="side-item-title">OpenClaw 配置</div>
2829
+ <div class="side-item-meta">
2830
+ <span>JSON5 / Workspace</span>
2831
+ <span v-if="currentOpenclawConfig">当前 {{ currentOpenclawConfig }}</span>
2832
+ </div>
2833
+ </button>
2834
+ </div>
2835
+
2836
+ <div class="side-section" role="tablist" aria-label="会话管理">
2837
+ <div class="side-section-title">会话管理</div>
2838
+ <button
2839
+ role="tab"
2840
+ id="side-tab-sessions"
2841
+ aria-controls="panel-sessions"
2842
+ :tabindex="mainTab === 'sessions' ? 0 : -1"
2843
+ :aria-selected="mainTab === 'sessions'"
2844
+ :aria-pressed="mainTab === 'sessions'"
2845
+ :class="['side-item', { active: mainTab === 'sessions' }]"
2846
+ @click="switchMainTab('sessions')">
2847
+ <div class="side-item-title">会话浏览</div>
2848
+ <div class="side-item-meta">
2849
+ <span>快速预览 / 导出</span>
2850
+ <span>来源:{{ sessionFilterSource === 'all' ? '全部' : (sessionFilterSource === 'codex' ? 'Codex' : 'Claude') }}</span>
2851
+ </div>
2852
+ </button>
2853
+ </div>
2854
+ </aside>
2437
2855
  <main class="main-panel">
2438
2856
  <div class="panel-header" v-if="!sessionStandalone">
2439
2857
  <h1 class="main-title">
@@ -2447,6 +2865,49 @@
2447
2865
  </p>
2448
2866
  </div>
2449
2867
 
2868
+ <div class="status-strip" v-if="!sessionStandalone && mainTab === 'config'">
2869
+ <template v-if="configMode === 'codex'">
2870
+ <div class="status-chip">
2871
+ <span class="label">Codex 提供商</span>
2872
+ <span class="value">{{ currentProvider || '未选择' }}</span>
2873
+ </div>
2874
+ <div class="status-chip">
2875
+ <span class="label">Codex 模型</span>
2876
+ <span class="value">{{ currentModel || '未选择' }}</span>
2877
+ </div>
2878
+ </template>
2879
+ <template v-else-if="configMode === 'claude'">
2880
+ <div class="status-chip">
2881
+ <span class="label">Claude 配置</span>
2882
+ <span class="value">{{ currentClaudeConfig || '未选择' }}</span>
2883
+ </div>
2884
+ <div class="status-chip">
2885
+ <span class="label">Claude 模型</span>
2886
+ <span class="value">{{ currentClaudeModel || '未选择' }}</span>
2887
+ </div>
2888
+ </template>
2889
+ <template v-else>
2890
+ <div class="status-chip">
2891
+ <span class="label">OpenClaw 配置</span>
2892
+ <span class="value">{{ currentOpenclawConfig || '未选择' }}</span>
2893
+ </div>
2894
+ <div class="status-chip">
2895
+ <span class="label">工作区文件</span>
2896
+ <span class="value">{{ openclawWorkspaceFileName || '未选择' }}</span>
2897
+ </div>
2898
+ </template>
2899
+ </div>
2900
+ <div class="status-strip" v-else-if="!sessionStandalone && mainTab === 'sessions'">
2901
+ <div class="status-chip">
2902
+ <span class="label">当前来源</span>
2903
+ <span class="value">{{ sessionFilterSource === 'claude' ? 'Claude Code' : 'Codex' }}</span>
2904
+ </div>
2905
+ <div class="status-chip">
2906
+ <span class="label">会话数</span>
2907
+ <span class="value">{{ sessionsList.length }}</span>
2908
+ </div>
2909
+ </div>
2910
+
2450
2911
  <div v-if="false && mainTab === 'config' && !sessionStandalone" class="config-subtabs">
2451
2912
  <button :class="['config-subtab', { active: configMode === 'codex' }]" @click="switchConfigMode('codex')">
2452
2913
  Codex 配置
@@ -2462,7 +2923,12 @@
2462
2923
  <!-- 内容包裹器 - 稳定布局 -->
2463
2924
  <div class="content-wrapper">
2464
2925
  <!-- Codex 配置模式 -->
2465
- <div v-show="mainTab === 'config' && configMode === 'codex'" class="mode-content mode-cards">
2926
+ <div
2927
+ v-show="mainTab === 'config' && configMode === 'codex'"
2928
+ class="mode-content mode-cards"
2929
+ id="panel-config-codex"
2930
+ role="tabpanel"
2931
+ :aria-labelledby="'tab-config-codex'">
2466
2932
  <!-- 添加提供商按钮 -->
2467
2933
  <button class="btn-add" @click="showAddModal = true" v-if="!loading && !initError">
2468
2934
  <svg class="icon" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2">
@@ -2597,8 +3063,13 @@
2597
3063
  </div>
2598
3064
  </div>
2599
3065
 
2600
- <!-- Claude Code 配置模式 -->
2601
- <div v-show="mainTab === 'config' && configMode === 'claude'" class="mode-content mode-cards">
3066
+ <!-- Claude Code 配置模式 -->
3067
+ <div
3068
+ v-show="mainTab === 'config' && configMode === 'claude'"
3069
+ class="mode-content mode-cards"
3070
+ id="panel-config-claude"
3071
+ role="tabpanel"
3072
+ :aria-labelledby="'tab-config-claude'">
2602
3073
  <!-- 添加提供商按钮 -->
2603
3074
  <button class="btn-add" @click="openClaudeConfigModal" v-if="!loading && !initError">
2604
3075
  <svg class="icon" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2">
@@ -2688,8 +3159,13 @@
2688
3159
  </div>
2689
3160
  </div>
2690
3161
 
2691
- <!-- OpenClaw 配置模式 -->
2692
- <div v-show="mainTab === 'config' && configMode === 'openclaw'" class="mode-content mode-cards">
3162
+ <!-- OpenClaw 配置模式 -->
3163
+ <div
3164
+ v-show="mainTab === 'config' && configMode === 'openclaw'"
3165
+ class="mode-content mode-cards"
3166
+ id="panel-config-openclaw"
3167
+ role="tabpanel"
3168
+ :aria-labelledby="'tab-config-openclaw'">
2693
3169
  <button class="btn-add" @click="openOpenclawAddModal" v-if="!loading && !initError">
2694
3170
  <svg class="icon" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2">
2695
3171
  <path d="M10 4v12M4 10h12"/>
@@ -2762,8 +3238,13 @@
2762
3238
  </div>
2763
3239
  </div>
2764
3240
 
2765
- <!-- 会话浏览模式 -->
2766
- <div v-show="mainTab === 'sessions'" class="mode-content">
3241
+ <!-- 会话浏览模式 -->
3242
+ <div
3243
+ v-show="mainTab === 'sessions'"
3244
+ class="mode-content"
3245
+ id="panel-sessions"
3246
+ role="tabpanel"
3247
+ :aria-labelledby="'tab-sessions'">
2767
3248
  <div v-if="sessionStandalone" class="session-standalone-page">
2768
3249
  <div v-if="sessionStandaloneLoading" class="state-message">
2769
3250
  加载中...
@@ -2788,10 +3269,9 @@
2788
3269
  <div class="session-toolbar">
2789
3270
  <div class="session-toolbar-group">
2790
3271
  <select class="session-source-select" v-model="sessionFilterSource" @change="onSessionSourceChange" :disabled="sessionsLoading">
2791
- <option value="all">全部(Codex + Claude)</option>
2792
- <option value="codex">仅 Codex</option>
2793
- <option value="claude">仅 Claude Code</option>
2794
- </select>
3272
+ <option value="codex">Codex</option>
3273
+ <option value="claude">Claude Code</option>
3274
+ </select>
2795
3275
  <select
2796
3276
  class="session-path-select"
2797
3277
  v-model="sessionPathFilter"
@@ -2865,13 +3345,13 @@
2865
3345
  <div
2866
3346
  v-for="session in sessionsList"
2867
3347
  :key="session.source + '-' + session.sessionId + '-' + session.filePath"
2868
- :class="[
2869
- 'session-item',
2870
- {
2871
- active: activeSession && getSessionExportKey(activeSession) === getSessionExportKey(session)
2872
- }
2873
- ]"
2874
- @click="selectSession(session)">
3348
+ :class="[
3349
+ 'session-item',
3350
+ {
3351
+ active: activeSession && getSessionExportKey(activeSession) === getSessionExportKey(session)
3352
+ }
3353
+ ]"
3354
+ @click="selectSession(session)">
2875
3355
  <div class="session-item-header">
2876
3356
  <div class="session-item-main">
2877
3357
  <div class="session-item-title">{{ session.title || session.sessionId }}</div>
@@ -2903,7 +3383,7 @@
2903
3383
  </div>
2904
3384
  </div>
2905
3385
 
2906
- <div class="session-preview">
3386
+ <div :class="['session-preview', { active: !!activeSession }]">
2907
3387
  <template v-if="activeSession">
2908
3388
  <div class="session-preview-scroll">
2909
3389
  <div class="session-preview-header">
@@ -3492,2926 +3972,6 @@
3492
3972
  <div v-if="message" :class="['toast', messageType]">{{ message }}</div>
3493
3973
  </div>
3494
3974
 
3495
- <script>
3496
- const { createApp } = Vue;
3497
- const API_BASE = (location && location.origin && location.origin !== 'null')
3498
- ? location.origin
3499
- : 'http://localhost:3737';
3500
- const DEFAULT_OPENCLAW_TEMPLATE = `{
3501
- // OpenClaw config (JSON5)
3502
- agent: {
3503
- model: "gpt-4.1"
3504
- },
3505
- agents: {
3506
- defaults: {
3507
- workspace: "~/.openclaw/workspace"
3508
- }
3509
- }
3510
- }`;
3511
-
3512
- async function api(action, params = {}) {
3513
- const res = await fetch(`${API_BASE}/api`, {
3514
- method: 'POST',
3515
- headers: { 'Content-Type': 'application/json' },
3516
- body: JSON.stringify({ action, params })
3517
- });
3518
- return await res.json();
3519
- }
3520
-
3521
- const app = createApp({
3522
- data() {
3523
- return {
3524
- mainTab: 'config',
3525
- configMode: 'codex',
3526
- currentProvider: '',
3527
- currentModel: '',
3528
- serviceTier: 'fast',
3529
- providersList: [],
3530
- models: [],
3531
- codexModelsLoading: false,
3532
- modelsSource: 'remote',
3533
- modelsHasCurrent: true,
3534
- claudeModels: [],
3535
- claudeModelsSource: 'idle',
3536
- claudeModelsHasCurrent: true,
3537
- claudeModelsLoading: false,
3538
- loading: true,
3539
- initError: '',
3540
- message: '',
3541
- messageType: '',
3542
- showAddModal: false,
3543
- showEditModal: false,
3544
- showModelModal: false,
3545
- showModelListModal: false,
3546
- showClaudeConfigModal: false,
3547
- showEditConfigModal: false,
3548
- showOpenclawConfigModal: false,
3549
- showConfigTemplateModal: false,
3550
- showAgentsModal: false,
3551
- configTemplateContent: '',
3552
- configTemplateApplying: false,
3553
- agentsContent: '',
3554
- agentsPath: '',
3555
- agentsExists: false,
3556
- agentsLineEnding: '\n',
3557
- agentsLoading: false,
3558
- agentsSaving: false,
3559
- agentsContext: 'codex',
3560
- agentsModalTitle: 'AGENTS.md 编辑器',
3561
- agentsModalHint: '保存后会写入目标 AGENTS.md(与 config.toml 同级)。',
3562
- sessionsList: [],
3563
- sessionsLoading: false,
3564
- sessionFilterSource: 'all',
3565
- sessionPathFilter: '',
3566
- sessionQuery: '',
3567
- sessionRoleFilter: 'all',
3568
- sessionTimePreset: 'all',
3569
- sessionResumeWithYolo: true,
3570
- sessionPathOptions: [],
3571
- sessionPathOptionsLoading: false,
3572
- sessionPathOptionsMap: {
3573
- all: [],
3574
- codex: [],
3575
- claude: []
3576
- },
3577
- sessionPathOptionsLoadedMap: {
3578
- all: false,
3579
- codex: false,
3580
- claude: false
3581
- },
3582
- sessionPathRequestSeq: 0,
3583
- sessionExporting: {},
3584
- sessionCloning: {},
3585
- sessionDeleting: {},
3586
- activeSession: null,
3587
- activeSessionMessages: [],
3588
- activeSessionDetailError: '',
3589
- activeSessionDetailClipped: false,
3590
- sessionDetailLoading: false,
3591
- sessionDetailRequestSeq: 0,
3592
- sessionStandalone: false,
3593
- sessionStandaloneError: '',
3594
- sessionStandaloneText: '',
3595
- sessionStandaloneTitle: '',
3596
- sessionStandaloneSourceLabel: '',
3597
- sessionStandaloneLoading: false,
3598
- sessionStandaloneRequestSeq: 0,
3599
- speedResults: {},
3600
- speedLoading: {},
3601
- claudeSpeedResults: {},
3602
- claudeSpeedLoading: {},
3603
- claudeShareLoading: {},
3604
- providerShareLoading: {},
3605
- newProvider: { name: '', url: '', key: '' },
3606
- editingProvider: { name: '', url: '', key: '' },
3607
- newModelName: '',
3608
- currentClaudeConfig: '',
3609
- currentClaudeModel: '',
3610
- editingConfig: { name: '', apiKey: '', baseUrl: '', model: '' },
3611
- claudeConfigs: {
3612
- '智谱GLM': {
3613
- apiKey: '',
3614
- baseUrl: 'https://open.bigmodel.cn/api/anthropic',
3615
- model: 'glm-4.7',
3616
- hasKey: false
3617
- }
3618
- },
3619
- newClaudeConfig: {
3620
- name: '',
3621
- apiKey: '',
3622
- baseUrl: 'https://open.bigmodel.cn/api/anthropic',
3623
- model: 'glm-4.7'
3624
- },
3625
- currentOpenclawConfig: '',
3626
- openclawConfigs: {
3627
- '默认配置': {
3628
- content: DEFAULT_OPENCLAW_TEMPLATE
3629
- }
3630
- },
3631
- openclawEditing: { name: '', content: '', lockName: false },
3632
- openclawEditorTitle: '添加 OpenClaw 配置',
3633
- openclawConfigPath: '',
3634
- openclawConfigExists: false,
3635
- openclawLineEnding: '\n',
3636
- openclawFileLoading: false,
3637
- openclawSaving: false,
3638
- openclawApplying: false,
3639
- openclawWorkspaceFileName: 'SOUL.md',
3640
- agentsWorkspaceFileName: '',
3641
- openclawStructured: {
3642
- agentPrimary: '',
3643
- agentFallbacks: [],
3644
- workspace: '',
3645
- timeout: '',
3646
- contextTokens: '',
3647
- maxConcurrent: '',
3648
- envItems: [],
3649
- toolsProfile: 'default',
3650
- toolsAllow: [],
3651
- toolsDeny: []
3652
- },
3653
- openclawQuick: {
3654
- providerName: '',
3655
- baseUrl: '',
3656
- apiKey: '',
3657
- apiType: 'openai-responses',
3658
- modelId: '',
3659
- modelName: '',
3660
- contextWindow: '',
3661
- maxTokens: '',
3662
- setPrimary: true,
3663
- overrideProvider: true,
3664
- overrideModels: true,
3665
- showKey: false
3666
- },
3667
- openclawAgentsList: [],
3668
- openclawProviders: [],
3669
- openclawMissingProviders: [],
3670
- healthCheckLoading: false,
3671
- healthCheckResult: null,
3672
- healthCheckRemote: false
3673
- }
3674
- },
3675
- mounted() {
3676
- this.initSessionStandalone();
3677
- const savedSessionYolo = localStorage.getItem('codexmateSessionResumeYolo');
3678
- if (savedSessionYolo === '0' || savedSessionYolo === 'false') {
3679
- this.sessionResumeWithYolo = false;
3680
- } else if (savedSessionYolo === '1' || savedSessionYolo === 'true') {
3681
- this.sessionResumeWithYolo = true;
3682
- }
3683
- const savedConfigs = localStorage.getItem('claudeConfigs');
3684
- if (savedConfigs) {
3685
- try {
3686
- this.claudeConfigs = JSON.parse(savedConfigs);
3687
- for (const [name, config] of Object.entries(this.claudeConfigs)) {
3688
- if (config.apiKey && config.apiKey.includes('****')) {
3689
- config.apiKey = '';
3690
- config.hasKey = false;
3691
- }
3692
- }
3693
- localStorage.setItem('claudeConfigs', JSON.stringify(this.claudeConfigs));
3694
- } catch (e) {
3695
- console.error('加载 Claude 配置失败:', e);
3696
- }
3697
- }
3698
- void this.refreshClaudeSelectionFromSettings({ silent: true });
3699
- const savedOpenclawConfigs = localStorage.getItem('openclawConfigs');
3700
- if (savedOpenclawConfigs) {
3701
- try {
3702
- this.openclawConfigs = JSON.parse(savedOpenclawConfigs);
3703
- const configNames = Object.keys(this.openclawConfigs);
3704
- if (configNames.length > 0) {
3705
- this.currentOpenclawConfig = configNames[0];
3706
- }
3707
- } catch (e) {
3708
- console.error('加载 OpenClaw 配置失败:', e);
3709
- }
3710
- } else {
3711
- const configNames = Object.keys(this.openclawConfigs);
3712
- if (configNames.length > 0) {
3713
- this.currentOpenclawConfig = configNames[0];
3714
- }
3715
- }
3716
- this.loadAll();
3717
- },
3718
-
3719
- computed: {
3720
- isSessionQueryEnabled() {
3721
- return this.sessionFilterSource === 'codex';
3722
- },
3723
- sessionQueryPlaceholder() {
3724
- return this.isSessionQueryEnabled ? '关键词检索' : '仅 Codex 支持关键词检索';
3725
- },
3726
- claudeModelHasList() {
3727
- return Array.isArray(this.claudeModels) && this.claudeModels.length > 0;
3728
- },
3729
- claudeModelOptions() {
3730
- const list = Array.isArray(this.claudeModels) ? [...this.claudeModels] : [];
3731
- const current = (this.currentClaudeModel || '').trim();
3732
- if (current && !list.includes(current)) {
3733
- list.unshift(current);
3734
- }
3735
- return list;
3736
- }
3737
- },
3738
- methods: {
3739
- async loadAll() {
3740
- this.loading = true;
3741
- this.initError = '';
3742
- try {
3743
- const statusRes = await api('status');
3744
- const listRes = await api('list');
3745
-
3746
- if (statusRes.error) {
3747
- this.initError = statusRes.error;
3748
- } else {
3749
- this.currentProvider = statusRes.provider;
3750
- this.currentModel = statusRes.model;
3751
- {
3752
- const tier = typeof statusRes.serviceTier === 'string'
3753
- ? statusRes.serviceTier.trim().toLowerCase()
3754
- : '';
3755
- this.serviceTier = tier === 'fast' ? 'fast' : (tier ? 'standard' : 'fast');
3756
- }
3757
- this.providersList = listRes.providers;
3758
- await this.loadModelsForProvider(this.currentProvider);
3759
- if (statusRes.configReady === false) {
3760
- this.showMessage(statusRes.configNotice || '未检测到 config.toml,已加载默认模板。请在模板编辑器确认后创建。', 'info');
3761
- }
3762
- if (statusRes.initNotice) {
3763
- this.showMessage(statusRes.initNotice, 'info');
3764
- }
3765
- this.maybeShowStarPrompt();
3766
- }
3767
- } catch (e) {
3768
- this.initError = '连接失败: ' + e.message;
3769
- } finally {
3770
- this.loading = false;
3771
- }
3772
- },
3773
-
3774
- async loadModelsForProvider(providerName) {
3775
- this.codexModelsLoading = true;
3776
- if (!providerName) {
3777
- this.models = [];
3778
- this.modelsSource = 'unlimited';
3779
- this.modelsHasCurrent = true;
3780
- this.codexModelsLoading = false;
3781
- return;
3782
- }
3783
- try {
3784
- const res = await api('models', { provider: providerName });
3785
- if (res.unlimited) {
3786
- this.models = [];
3787
- this.modelsSource = 'unlimited';
3788
- this.modelsHasCurrent = true;
3789
- return;
3790
- }
3791
- if (res.error) {
3792
- this.showMessage('模型列表获取失败: ' + res.error, 'error');
3793
- this.models = [];
3794
- this.modelsSource = 'error';
3795
- this.modelsHasCurrent = true;
3796
- return;
3797
- }
3798
- const list = Array.isArray(res.models) ? res.models : [];
3799
- this.models = list;
3800
- this.modelsSource = res.source || 'remote';
3801
- this.modelsHasCurrent = !!this.currentModel && list.includes(this.currentModel);
3802
- } catch (e) {
3803
- this.showMessage('模型列表获取失败: ' + e.message, 'error');
3804
- this.models = [];
3805
- this.modelsSource = 'error';
3806
- this.modelsHasCurrent = true;
3807
- } finally {
3808
- this.codexModelsLoading = false;
3809
- }
3810
- },
3811
-
3812
- getCurrentClaudeConfig() {
3813
- if (!this.currentClaudeConfig) return null;
3814
- return this.claudeConfigs[this.currentClaudeConfig] || null;
3815
- },
3816
-
3817
- normalizeClaudeValue(value) {
3818
- return typeof value === 'string' ? value.trim() : '';
3819
- },
3820
-
3821
- normalizeClaudeConfig(config) {
3822
- const safe = config && typeof config === 'object' ? config : {};
3823
- return {
3824
- apiKey: this.normalizeClaudeValue(safe.apiKey),
3825
- baseUrl: this.normalizeClaudeValue(safe.baseUrl),
3826
- model: this.normalizeClaudeValue(safe.model)
3827
- };
3828
- },
3829
-
3830
- normalizeClaudeSettingsEnv(env) {
3831
- const safe = env && typeof env === 'object' ? env : {};
3832
- return {
3833
- apiKey: this.normalizeClaudeValue(safe.ANTHROPIC_API_KEY),
3834
- baseUrl: this.normalizeClaudeValue(safe.ANTHROPIC_BASE_URL),
3835
- model: this.normalizeClaudeValue(safe.ANTHROPIC_MODEL)
3836
- };
3837
- },
3838
-
3839
- matchClaudeConfigFromSettings(env) {
3840
- const normalizedSettings = this.normalizeClaudeSettingsEnv(env);
3841
- if (!normalizedSettings.apiKey || !normalizedSettings.baseUrl || !normalizedSettings.model) {
3842
- return '';
3843
- }
3844
- const entries = Object.entries(this.claudeConfigs || {});
3845
- for (const [name, config] of entries) {
3846
- const normalizedConfig = this.normalizeClaudeConfig(config);
3847
- if (!normalizedConfig.apiKey || !normalizedConfig.baseUrl || !normalizedConfig.model) {
3848
- continue;
3849
- }
3850
- if (normalizedConfig.apiKey === normalizedSettings.apiKey
3851
- && normalizedConfig.baseUrl === normalizedSettings.baseUrl
3852
- && normalizedConfig.model === normalizedSettings.model) {
3853
- return name;
3854
- }
3855
- }
3856
- return '';
3857
- },
3858
-
3859
- findDuplicateClaudeConfigName(config) {
3860
- const normalized = this.normalizeClaudeConfig(config);
3861
- if (!normalized.apiKey || !normalized.baseUrl || !normalized.model) {
3862
- return '';
3863
- }
3864
- const entries = Object.entries(this.claudeConfigs || {});
3865
- for (const [name, existing] of entries) {
3866
- const normalizedExisting = this.normalizeClaudeConfig(existing);
3867
- if (!normalizedExisting.apiKey || !normalizedExisting.baseUrl || !normalizedExisting.model) {
3868
- continue;
3869
- }
3870
- if (normalizedExisting.apiKey === normalized.apiKey
3871
- && normalizedExisting.baseUrl === normalized.baseUrl
3872
- && normalizedExisting.model === normalized.model) {
3873
- return name;
3874
- }
3875
- }
3876
- return '';
3877
- },
3878
-
3879
- async refreshClaudeSelectionFromSettings(options = {}) {
3880
- const configNames = Object.keys(this.claudeConfigs || {});
3881
- if (configNames.length === 0) {
3882
- this.currentClaudeConfig = '';
3883
- this.currentClaudeModel = '';
3884
- this.resetClaudeModelsState();
3885
- return;
3886
- }
3887
- const silent = !!options.silent;
3888
- try {
3889
- const res = await api('get-claude-settings');
3890
- if (res && res.error) {
3891
- if (!silent) {
3892
- this.showMessage('读取 Claude 配置失败: ' + res.error, 'error');
3893
- }
3894
- return;
3895
- }
3896
- const matchName = this.matchClaudeConfigFromSettings((res && res.env) || {});
3897
- if (matchName) {
3898
- if (this.currentClaudeConfig !== matchName) {
3899
- this.currentClaudeConfig = matchName;
3900
- }
3901
- this.refreshClaudeModelContext();
3902
- return;
3903
- }
3904
- this.currentClaudeConfig = '';
3905
- this.currentClaudeModel = '';
3906
- this.resetClaudeModelsState();
3907
- if (!silent) {
3908
- const tip = res && res.exists
3909
- ? '当前 Claude settings.json 与本地配置不匹配,已取消选中'
3910
- : '未检测到 Claude settings.json,已取消选中';
3911
- this.showMessage(tip, 'info');
3912
- }
3913
- } catch (e) {
3914
- if (!silent) {
3915
- this.showMessage('读取 Claude 配置失败: ' + e.message, 'error');
3916
- }
3917
- }
3918
- },
3919
-
3920
- syncClaudeModelFromConfig() {
3921
- const config = this.getCurrentClaudeConfig();
3922
- this.currentClaudeModel = config && config.model ? config.model : '';
3923
- },
3924
-
3925
- refreshClaudeModelContext() {
3926
- this.syncClaudeModelFromConfig();
3927
- this.loadClaudeModels();
3928
- },
3929
-
3930
- resetClaudeModelsState() {
3931
- this.claudeModels = [];
3932
- this.claudeModelsSource = 'idle';
3933
- this.claudeModelsHasCurrent = true;
3934
- this.claudeModelsLoading = false;
3935
- },
3936
-
3937
- updateClaudeModelsCurrent() {
3938
- const currentModel = (this.currentClaudeModel || '').trim();
3939
- this.claudeModelsHasCurrent = !!currentModel && this.claudeModels.includes(currentModel);
3940
- },
3941
-
3942
- async loadClaudeModels() {
3943
- const config = this.getCurrentClaudeConfig();
3944
- if (!config) {
3945
- this.resetClaudeModelsState();
3946
- return;
3947
- }
3948
- const baseUrl = (config.baseUrl || '').trim();
3949
- const apiKey = (config.apiKey || '').trim();
3950
-
3951
- if (!baseUrl) {
3952
- this.resetClaudeModelsState();
3953
- return;
3954
- }
3955
-
3956
- this.claudeModelsLoading = true;
3957
- try {
3958
- const res = await api('models-by-url', { baseUrl, apiKey });
3959
- if (res.unlimited) {
3960
- this.claudeModels = [];
3961
- this.claudeModelsSource = 'unlimited';
3962
- this.claudeModelsHasCurrent = true;
3963
- return;
3964
- }
3965
- if (res.error) {
3966
- this.showMessage('模型列表获取失败: ' + res.error, 'error');
3967
- this.claudeModels = [];
3968
- this.claudeModelsSource = 'error';
3969
- this.claudeModelsHasCurrent = true;
3970
- return;
3971
- }
3972
- const list = Array.isArray(res.models) ? res.models : [];
3973
- this.claudeModels = list;
3974
- this.claudeModelsSource = res.source || 'remote';
3975
- this.updateClaudeModelsCurrent();
3976
- } catch (e) {
3977
- this.showMessage('模型列表获取失败: ' + e.message, 'error');
3978
- this.claudeModels = [];
3979
- this.claudeModelsSource = 'error';
3980
- this.claudeModelsHasCurrent = true;
3981
- } finally {
3982
- this.claudeModelsLoading = false;
3983
- }
3984
- },
3985
-
3986
- openClaudeConfigModal() {
3987
- this.showClaudeConfigModal = true;
3988
- },
3989
-
3990
- maybeShowStarPrompt() {
3991
- const storageKey = 'codexmateStarPrompted';
3992
- if (localStorage.getItem(storageKey)) {
3993
- return;
3994
- }
3995
- this.showMessage('如果 Codex Mate 对你有帮助,欢迎到 GitHub 点个 Star。', 'info');
3996
- localStorage.setItem(storageKey, '1');
3997
- },
3998
-
3999
- switchConfigMode(mode) {
4000
- this.mainTab = 'config';
4001
- this.configMode = mode;
4002
- if (mode === 'claude') {
4003
- this.refreshClaudeModelContext();
4004
- }
4005
- },
4006
-
4007
- switchMainTab(tab) {
4008
- this.mainTab = tab;
4009
- if (tab === 'sessions' && this.sessionsList.length === 0) {
4010
- this.loadSessions();
4011
- }
4012
- if (tab === 'config' && this.configMode === 'claude') {
4013
- this.refreshClaudeModelContext();
4014
- }
4015
- },
4016
-
4017
- getSessionStandaloneContext() {
4018
- try {
4019
- const url = new URL(window.location.href);
4020
- if (url.pathname !== '/session') {
4021
- return { requested: false, params: null, error: '' };
4022
- }
4023
-
4024
- const source = (url.searchParams.get('source') || '').trim().toLowerCase();
4025
- const sessionId = (url.searchParams.get('sessionId') || url.searchParams.get('id') || '').trim();
4026
- const filePath = (url.searchParams.get('filePath') || url.searchParams.get('path') || '').trim();
4027
- let error = '';
4028
- if (!source) {
4029
- error = '缺少 source 参数';
4030
- } else if (source !== 'codex' && source !== 'claude') {
4031
- error = 'source 仅支持 codex 或 claude';
4032
- }
4033
- if (!sessionId && !filePath) {
4034
- error = error ? `${error},还缺少 sessionId 或 filePath` : '缺少 sessionId 或 filePath 参数';
4035
- }
4036
-
4037
- if (error) {
4038
- return { requested: true, params: null, error };
4039
- }
4040
-
4041
- return {
4042
- requested: true,
4043
- params: {
4044
- source,
4045
- sessionId,
4046
- filePath
4047
- },
4048
- error: ''
4049
- };
4050
- } catch (_) {
4051
- return { requested: false, params: null, error: '' };
4052
- }
4053
- },
4054
-
4055
- initSessionStandalone() {
4056
- const context = this.getSessionStandaloneContext();
4057
- if (!context.requested) return;
4058
-
4059
- this.sessionStandalone = true;
4060
- this.mainTab = 'sessions';
4061
-
4062
- if (context.error || !context.params) {
4063
- this.sessionStandaloneError = `会话链接参数不完整:${context.error || '参数解析失败'}`;
4064
- return;
4065
- }
4066
-
4067
- const sourceLabel = context.params.source === 'codex' ? 'Codex' : 'Claude Code';
4068
- this.activeSession = {
4069
- source: context.params.source,
4070
- sourceLabel,
4071
- sessionId: context.params.sessionId,
4072
- filePath: context.params.filePath,
4073
- title: context.params.sessionId || context.params.filePath || '会话'
4074
- };
4075
- this.activeSessionMessages = [];
4076
- this.activeSessionDetailError = '';
4077
- this.activeSessionDetailClipped = false;
4078
- this.sessionStandaloneError = '';
4079
- this.sessionStandaloneText = '';
4080
- this.sessionStandaloneTitle = this.activeSession.title || '会话';
4081
- this.sessionStandaloneSourceLabel = sourceLabel;
4082
- this.loadSessionStandalonePlain();
4083
- },
4084
-
4085
- buildSessionStandaloneUrl(session) {
4086
- if (!session) return '';
4087
- const source = typeof session.source === 'string' ? session.source.trim().toLowerCase() : '';
4088
- if (!source || (source !== 'codex' && source !== 'claude')) return '';
4089
- const sessionId = typeof session.sessionId === 'string' ? session.sessionId.trim() : '';
4090
- const filePath = typeof session.filePath === 'string' ? session.filePath.trim() : '';
4091
- if (!sessionId && !filePath) return '';
4092
- const origin = window.location.origin && window.location.origin !== 'null'
4093
- ? window.location.origin
4094
- : API_BASE;
4095
- const params = new URLSearchParams();
4096
- params.set('source', source);
4097
- if (sessionId) params.set('sessionId', sessionId);
4098
- if (filePath) params.set('filePath', filePath);
4099
- return `${origin}/session?${params.toString()}`;
4100
- },
4101
-
4102
- openSessionStandalone(session) {
4103
- const url = this.buildSessionStandaloneUrl(session);
4104
- if (!url) {
4105
- this.showMessage('当前会话无法生成新页链接', 'error');
4106
- return;
4107
- }
4108
- window.open(url, '_blank', 'noopener');
4109
- },
4110
-
4111
- getSessionExportKey(session) {
4112
- return `${session.source || 'unknown'}:${session.sessionId || ''}:${session.filePath || ''}`;
4113
- },
4114
-
4115
- isResumeCommandAvailable(session) {
4116
- if (!session) return false;
4117
- const source = String(session.source || '').trim().toLowerCase();
4118
- const sessionId = typeof session.sessionId === 'string' ? session.sessionId.trim() : '';
4119
- return source === 'codex' && !!sessionId;
4120
- },
4121
-
4122
- isCloneAvailable(session) {
4123
- if (!session) return false;
4124
- const source = String(session.source || '').trim().toLowerCase();
4125
- const sessionId = typeof session.sessionId === 'string' ? session.sessionId.trim() : '';
4126
- const filePath = typeof session.filePath === 'string' ? session.filePath.trim() : '';
4127
- return source === 'codex' && (!!sessionId || !!filePath);
4128
- },
4129
-
4130
- isDeleteAvailable(session) {
4131
- if (!session) return false;
4132
- const source = String(session.source || '').trim().toLowerCase();
4133
- if (source !== 'codex' && source !== 'claude') return false;
4134
- const sessionId = typeof session.sessionId === 'string' ? session.sessionId.trim() : '';
4135
- const filePath = typeof session.filePath === 'string' ? session.filePath.trim() : '';
4136
- return !!sessionId || !!filePath;
4137
- },
4138
-
4139
- buildResumeCommand(session) {
4140
- const sessionId = session && session.sessionId ? String(session.sessionId).trim() : '';
4141
- const arg = this.quoteResumeArg(sessionId);
4142
- if (this.sessionResumeWithYolo) {
4143
- return `codex --yolo resume ${arg}`;
4144
- }
4145
- return `codex resume ${arg}`;
4146
- },
4147
-
4148
- quoteShellArg(value) {
4149
- const text = typeof value === 'string' ? value : String(value || '');
4150
- if (!text) return "''";
4151
- if (/^[a-zA-Z0-9._-]+$/.test(text)) return text;
4152
- const escaped = text.replace(/'/g, "'\\''");
4153
- return `'${escaped}'`;
4154
- },
4155
-
4156
- quoteResumeArg(value) {
4157
- return this.quoteShellArg(value);
4158
- },
4159
-
4160
- fallbackCopyText(text) {
4161
- let textarea = null;
4162
- try {
4163
- textarea = document.createElement('textarea');
4164
- textarea.value = text;
4165
- textarea.setAttribute('readonly', '');
4166
- textarea.style.position = 'fixed';
4167
- textarea.style.top = '-9999px';
4168
- textarea.style.left = '-9999px';
4169
- textarea.style.opacity = '0';
4170
- document.body.appendChild(textarea);
4171
- textarea.select();
4172
- textarea.setSelectionRange(0, textarea.value.length);
4173
- return document.execCommand('copy');
4174
- } catch (e) {
4175
- return false;
4176
- } finally {
4177
- if (textarea && textarea.parentNode) {
4178
- textarea.parentNode.removeChild(textarea);
4179
- }
4180
- }
4181
- },
4182
-
4183
- copyAgentsContent() {
4184
- const text = typeof this.agentsContent === 'string' ? this.agentsContent : '';
4185
- if (!text) {
4186
- this.showMessage('没有可复制的内容', 'info');
4187
- return;
4188
- }
4189
- const ok = this.fallbackCopyText(text);
4190
- if (ok) {
4191
- this.showMessage('已复制 AGENTS.md 内容', 'success');
4192
- return;
4193
- }
4194
- this.showMessage('复制失败,请手动复制内容', 'error');
4195
- },
4196
-
4197
- async copyResumeCommand(session) {
4198
- if (!this.isResumeCommandAvailable(session)) {
4199
- this.showMessage('当前会话不支持生成恢复命令', 'error');
4200
- return;
4201
- }
4202
- const command = this.buildResumeCommand(session);
4203
- const ok = this.fallbackCopyText(command);
4204
- if (ok) {
4205
- this.showMessage('已复制恢复命令', 'success');
4206
- return;
4207
- }
4208
- try {
4209
- if (navigator.clipboard && window.isSecureContext) {
4210
- await navigator.clipboard.writeText(command);
4211
- this.showMessage('已复制恢复命令', 'success');
4212
- return;
4213
- }
4214
- } catch (e) {
4215
- // keep fallback failure message
4216
- }
4217
- this.showMessage('复制失败,请手动复制命令', 'error');
4218
- },
4219
-
4220
- buildProviderShareCommand(payload) {
4221
- if (!payload || typeof payload !== 'object') return '';
4222
- const name = typeof payload.name === 'string' ? payload.name.trim() : '';
4223
- const baseUrl = typeof payload.baseUrl === 'string' ? payload.baseUrl.trim() : '';
4224
- const apiKey = typeof payload.apiKey === 'string' ? payload.apiKey : '';
4225
- if (!name || !baseUrl) return '';
4226
-
4227
- const nameArg = this.quoteShellArg(name);
4228
- const urlArg = this.quoteShellArg(baseUrl);
4229
- const keyArg = apiKey ? this.quoteShellArg(apiKey) : '';
4230
- const switchCmd = `codexmate switch ${nameArg}`;
4231
- const addCmd = apiKey
4232
- ? `codexmate add ${nameArg} ${urlArg} ${keyArg}`
4233
- : `codexmate add ${nameArg} ${urlArg}`;
4234
- return `${addCmd} && ${switchCmd}`;
4235
- },
4236
-
4237
- buildClaudeShareCommand(payload) {
4238
- if (!payload || typeof payload !== 'object') return '';
4239
- const baseUrl = typeof payload.baseUrl === 'string' ? payload.baseUrl.trim() : '';
4240
- const apiKey = typeof payload.apiKey === 'string' ? payload.apiKey : '';
4241
- const model = typeof payload.model === 'string' && payload.model.trim()
4242
- ? payload.model.trim()
4243
- : 'glm-4.7';
4244
- if (!baseUrl || !apiKey) return '';
4245
- const urlArg = this.quoteShellArg(baseUrl);
4246
- const keyArg = this.quoteShellArg(apiKey);
4247
- const modelArg = this.quoteShellArg(model);
4248
- return `codexmate claude ${urlArg} ${keyArg} ${modelArg}`;
4249
- },
4250
-
4251
- async copyProviderShareCommand(provider) {
4252
- const name = provider && typeof provider.name === 'string' ? provider.name.trim() : '';
4253
- if (!name) {
4254
- this.showMessage('提供商名称无效', 'error');
4255
- return;
4256
- }
4257
- if (this.providerShareLoading[name]) {
4258
- return;
4259
- }
4260
- this.providerShareLoading[name] = true;
4261
- try {
4262
- const res = await api('export-provider', { name });
4263
- if (res && res.error) {
4264
- this.showMessage(res.error, 'error');
4265
- return;
4266
- }
4267
- const command = this.buildProviderShareCommand(res && res.payload ? res.payload : null);
4268
- if (!command) {
4269
- this.showMessage('分享命令生成失败', 'error');
4270
- return;
4271
- }
4272
- const ok = this.fallbackCopyText(command);
4273
- if (ok) {
4274
- this.showMessage('已复制分享命令', 'success');
4275
- return;
4276
- }
4277
- try {
4278
- if (navigator.clipboard && window.isSecureContext) {
4279
- await navigator.clipboard.writeText(command);
4280
- this.showMessage('已复制分享命令', 'success');
4281
- return;
4282
- }
4283
- } catch (e) {
4284
- // keep fallback failure message
4285
- }
4286
- this.showMessage('复制失败,请手动复制命令', 'error');
4287
- } catch (e) {
4288
- this.showMessage('生成分享命令失败: ' + e.message, 'error');
4289
- } finally {
4290
- this.providerShareLoading[name] = false;
4291
- }
4292
- },
4293
-
4294
- async copyClaudeShareCommand(name) {
4295
- const config = this.claudeConfigs[name];
4296
- if (!config) {
4297
- this.showMessage('配置不存在', 'error');
4298
- return;
4299
- }
4300
- if (this.claudeShareLoading[name]) return;
4301
- this.claudeShareLoading[name] = true;
4302
- try {
4303
- const res = await api('export-claude-share', { config });
4304
- if (res && res.error) {
4305
- this.showMessage(res.error, 'error');
4306
- return;
4307
- }
4308
- const command = this.buildClaudeShareCommand(res && res.payload ? res.payload : null);
4309
- if (!command) {
4310
- this.showMessage('分享命令生成失败', 'error');
4311
- return;
4312
- }
4313
- const ok = this.fallbackCopyText(command);
4314
- if (ok) {
4315
- this.showMessage('已复制分享命令', 'success');
4316
- return;
4317
- }
4318
- try {
4319
- if (navigator.clipboard && window.isSecureContext) {
4320
- await navigator.clipboard.writeText(command);
4321
- this.showMessage('已复制分享命令', 'success');
4322
- return;
4323
- }
4324
- } catch (e) {
4325
- // fall through
4326
- }
4327
- this.showMessage('复制失败,请手动复制命令', 'error');
4328
- } catch (e) {
4329
- this.showMessage('生成分享命令失败: ' + e.message, 'error');
4330
- } finally {
4331
- this.claudeShareLoading[name] = false;
4332
- }
4333
- },
4334
-
4335
- async cloneSession(session) {
4336
- if (!this.isCloneAvailable(session)) {
4337
- this.showMessage('当前会话不支持克隆', 'error');
4338
- return;
4339
- }
4340
- const key = this.getSessionExportKey(session);
4341
- if (this.sessionCloning[key]) {
4342
- return;
4343
- }
4344
- this.sessionCloning[key] = true;
4345
- try {
4346
- const res = await api('clone-session', {
4347
- source: session.source,
4348
- sessionId: session.sessionId,
4349
- filePath: session.filePath
4350
- });
4351
- if (res.error) {
4352
- this.showMessage(res.error, 'error');
4353
- return;
4354
- }
4355
-
4356
- this.showMessage('会话已克隆', 'success');
4357
- await this.loadSessions();
4358
- if (res.sessionId) {
4359
- const matched = this.sessionsList.find(item => item.source === 'codex' && item.sessionId === res.sessionId);
4360
- if (matched) {
4361
- await this.selectSession(matched);
4362
- }
4363
- }
4364
- } catch (e) {
4365
- this.showMessage('克隆失败: ' + e.message, 'error');
4366
- } finally {
4367
- this.sessionCloning[key] = false;
4368
- }
4369
- },
4370
-
4371
- async deleteSession(session) {
4372
- if (!this.isDeleteAvailable(session)) {
4373
- this.showMessage('当前会话不支持删除', 'error');
4374
- return;
4375
- }
4376
- const key = this.getSessionExportKey(session);
4377
- if (this.sessionDeleting[key]) {
4378
- return;
4379
- }
4380
- this.sessionDeleting[key] = true;
4381
- try {
4382
- const res = await api('delete-session', {
4383
- source: session.source,
4384
- sessionId: session.sessionId,
4385
- filePath: session.filePath
4386
- });
4387
- if (res.error) {
4388
- this.showMessage(res.error, 'error');
4389
- return;
4390
- }
4391
- this.showMessage('会话已删除', 'success');
4392
- await this.loadSessions();
4393
- } catch (e) {
4394
- this.showMessage('删除失败: ' + e.message, 'error');
4395
- } finally {
4396
- this.sessionDeleting[key] = false;
4397
- }
4398
- },
4399
-
4400
- normalizeSessionPathValue(value) {
4401
- if (typeof value !== 'string') return '';
4402
- return value.trim();
4403
- },
4404
-
4405
- mergeSessionPathOptions(baseList = [], incomingList = []) {
4406
- const merged = [];
4407
- const seen = new Set();
4408
- const append = (items) => {
4409
- if (!Array.isArray(items)) return;
4410
- for (const item of items) {
4411
- const value = this.normalizeSessionPathValue(item);
4412
- if (!value) continue;
4413
- const key = value.toLowerCase();
4414
- if (seen.has(key)) continue;
4415
- seen.add(key);
4416
- merged.push(value);
4417
- }
4418
- };
4419
-
4420
- append(baseList);
4421
- append(incomingList);
4422
- return merged;
4423
- },
4424
-
4425
- extractPathOptionsFromSessions(sessions) {
4426
- const paths = [];
4427
- if (!Array.isArray(sessions)) {
4428
- return paths;
4429
- }
4430
-
4431
- const seen = new Set();
4432
- for (const session of sessions) {
4433
- const value = this.normalizeSessionPathValue(session && session.cwd ? session.cwd : '');
4434
- if (!value) continue;
4435
- const key = value.toLowerCase();
4436
- if (seen.has(key)) continue;
4437
- seen.add(key);
4438
- paths.push(value);
4439
- }
4440
- return paths;
4441
- },
4442
-
4443
- syncSessionPathOptionsForSource(source, nextOptions, mergeWithExisting = false) {
4444
- const targetSource = source === 'codex' || source === 'claude' ? source : 'all';
4445
- const current = Array.isArray(this.sessionPathOptionsMap[targetSource])
4446
- ? this.sessionPathOptionsMap[targetSource]
4447
- : [];
4448
- const merged = mergeWithExisting
4449
- ? this.mergeSessionPathOptions(current, nextOptions)
4450
- : this.mergeSessionPathOptions([], nextOptions);
4451
- this.sessionPathOptionsMap = {
4452
- ...this.sessionPathOptionsMap,
4453
- [targetSource]: merged
4454
- };
4455
- this.refreshSessionPathOptions(targetSource);
4456
- },
4457
-
4458
- refreshSessionPathOptions(source) {
4459
- const targetSource = source === 'codex' || source === 'claude' ? source : 'all';
4460
- const base = Array.isArray(this.sessionPathOptionsMap[targetSource])
4461
- ? [...this.sessionPathOptionsMap[targetSource]]
4462
- : [];
4463
- const selected = this.normalizeSessionPathValue(this.sessionPathFilter);
4464
- if (selected && !base.some(item => item.toLowerCase() === selected.toLowerCase())) {
4465
- base.unshift(selected);
4466
- }
4467
- if (targetSource === this.sessionFilterSource) {
4468
- this.sessionPathOptions = base;
4469
- }
4470
- },
4471
-
4472
- async loadSessionPathOptions(options = {}) {
4473
- const source = options.source === 'codex' || options.source === 'claude'
4474
- ? options.source
4475
- : 'all';
4476
- const forceRefresh = !!options.forceRefresh;
4477
- const loaded = !!this.sessionPathOptionsLoadedMap[source];
4478
- if (!forceRefresh && loaded) {
4479
- return;
4480
- }
4481
-
4482
- const requestSeq = ++this.sessionPathRequestSeq;
4483
- this.sessionPathOptionsLoading = true;
4484
- try {
4485
- const res = await api('list-session-paths', {
4486
- source,
4487
- limit: 500,
4488
- forceRefresh
4489
- });
4490
- if (requestSeq !== this.sessionPathRequestSeq) {
4491
- return;
4492
- }
4493
- if (res && !res.error && Array.isArray(res.paths)) {
4494
- this.syncSessionPathOptionsForSource(source, res.paths, true);
4495
- this.sessionPathOptionsLoadedMap = {
4496
- ...this.sessionPathOptionsLoadedMap,
4497
- [source]: true
4498
- };
4499
- }
4500
- } catch (_) {
4501
- // 路径补全失败不影响会话主流程
4502
- } finally {
4503
- if (requestSeq === this.sessionPathRequestSeq) {
4504
- this.sessionPathOptionsLoading = false;
4505
- }
4506
- }
4507
- },
4508
-
4509
- onSessionResumeYoloChange() {
4510
- const value = this.sessionResumeWithYolo ? '1' : '0';
4511
- localStorage.setItem('codexmateSessionResumeYolo', value);
4512
- },
4513
-
4514
- async onSessionSourceChange() {
4515
- this.refreshSessionPathOptions(this.sessionFilterSource);
4516
- await this.loadSessions();
4517
- },
4518
-
4519
- async onSessionPathFilterChange() {
4520
- await this.loadSessions();
4521
- },
4522
-
4523
- async onSessionFilterChange() {
4524
- await this.loadSessions();
4525
- },
4526
-
4527
- async clearSessionFilters() {
4528
- this.sessionFilterSource = 'all';
4529
- this.sessionPathFilter = '';
4530
- this.sessionQuery = '';
4531
- this.sessionRoleFilter = 'all';
4532
- this.sessionTimePreset = 'all';
4533
- await this.onSessionSourceChange();
4534
- },
4535
-
4536
- getRecordKey(message) {
4537
- if (!message || !Number.isInteger(message.recordLineIndex) || message.recordLineIndex < 0) {
4538
- return '';
4539
- }
4540
- return String(message.recordLineIndex);
4541
- },
4542
-
4543
- getRecordRenderKey(message, idx) {
4544
- const recordKey = this.getRecordKey(message);
4545
- if (recordKey) {
4546
- return `record-${recordKey}`;
4547
- }
4548
- return `record-fallback-${idx}-${message && message.timestamp ? message.timestamp : ''}`;
4549
- },
4550
-
4551
- syncActiveSessionMessageCount(messageCount) {
4552
- if (!Number.isFinite(messageCount) || messageCount < 0) return;
4553
- if (this.activeSession) {
4554
- this.activeSession.messageCount = messageCount;
4555
- }
4556
- const activeKey = this.activeSession ? this.getSessionExportKey(this.activeSession) : '';
4557
- if (!activeKey) return;
4558
- const matched = this.sessionsList.find(item => this.getSessionExportKey(item) === activeKey);
4559
- if (matched) {
4560
- matched.messageCount = messageCount;
4561
- }
4562
- },
4563
-
4564
- async loadSessions() {
4565
- if (this.sessionsLoading) return;
4566
- this.sessionsLoading = true;
4567
- this.activeSessionDetailError = '';
4568
- const query = this.isSessionQueryEnabled ? this.sessionQuery : '';
4569
- try {
4570
- const res = await api('list-sessions', {
4571
- source: this.sessionFilterSource,
4572
- pathFilter: this.sessionPathFilter,
4573
- query,
4574
- queryMode: 'and',
4575
- queryScope: 'content',
4576
- contentScanLimit: 50,
4577
- roleFilter: this.sessionRoleFilter,
4578
- timeRangePreset: this.sessionTimePreset,
4579
- limit: 200,
4580
- forceRefresh: true
4581
- });
4582
- if (res.error) {
4583
- this.showMessage(res.error, 'error');
4584
- this.sessionsList = [];
4585
- this.activeSession = null;
4586
- this.activeSessionMessages = [];
4587
- this.activeSessionDetailClipped = false;
4588
- } else {
4589
- this.sessionsList = Array.isArray(res.sessions) ? res.sessions : [];
4590
- this.syncSessionPathOptionsForSource(
4591
- this.sessionFilterSource,
4592
- this.extractPathOptionsFromSessions(this.sessionsList),
4593
- true
4594
- );
4595
- if (this.sessionsList.length === 0) {
4596
- this.activeSession = null;
4597
- this.activeSessionMessages = [];
4598
- this.activeSessionDetailClipped = false;
4599
- } else {
4600
- const oldKey = this.activeSession ? this.getSessionExportKey(this.activeSession) : '';
4601
- const matched = this.sessionsList.find(item => this.getSessionExportKey(item) === oldKey);
4602
- this.activeSession = matched || this.sessionsList[0];
4603
- await this.loadActiveSessionDetail();
4604
- }
4605
- void this.loadSessionPathOptions({ source: this.sessionFilterSource });
4606
- }
4607
- } catch (e) {
4608
- this.sessionsList = [];
4609
- this.activeSession = null;
4610
- this.activeSessionMessages = [];
4611
- this.activeSessionDetailClipped = false;
4612
- this.showMessage('加载会话失败: ' + e.message, 'error');
4613
- } finally {
4614
- this.sessionsLoading = false;
4615
- }
4616
- },
4617
-
4618
- async selectSession(session) {
4619
- if (!session) return;
4620
- if (this.activeSession && this.getSessionExportKey(this.activeSession) === this.getSessionExportKey(session)) return;
4621
- this.activeSession = session;
4622
- this.activeSessionMessages = [];
4623
- this.activeSessionDetailError = '';
4624
- this.activeSessionDetailClipped = false;
4625
- await this.loadActiveSessionDetail();
4626
- },
4627
-
4628
- async loadSessionStandalonePlain() {
4629
- if (!this.activeSession) {
4630
- this.sessionStandaloneText = '';
4631
- this.sessionStandaloneTitle = '会话';
4632
- this.sessionStandaloneSourceLabel = '';
4633
- this.sessionStandaloneError = '';
4634
- return;
4635
- }
4636
-
4637
- const requestSeq = ++this.sessionStandaloneRequestSeq;
4638
- this.sessionStandaloneLoading = true;
4639
- this.sessionStandaloneError = '';
4640
- try {
4641
- const res = await api('session-plain', {
4642
- source: this.activeSession.source,
4643
- sessionId: this.activeSession.sessionId,
4644
- filePath: this.activeSession.filePath
4645
- });
4646
-
4647
- if (requestSeq !== this.sessionStandaloneRequestSeq) {
4648
- return;
4649
- }
4650
-
4651
- if (res.error) {
4652
- this.sessionStandaloneText = '';
4653
- this.sessionStandaloneError = res.error;
4654
- return;
4655
- }
4656
-
4657
- this.sessionStandaloneSourceLabel = res.sourceLabel || this.activeSession.sourceLabel || '';
4658
- this.sessionStandaloneTitle = res.sessionId || this.activeSession.title || '会话';
4659
- this.sessionStandaloneText = typeof res.text === 'string' ? res.text : '';
4660
- } catch (e) {
4661
- if (requestSeq !== this.sessionStandaloneRequestSeq) {
4662
- return;
4663
- }
4664
- this.sessionStandaloneText = '';
4665
- this.sessionStandaloneError = '加载会话内容失败: ' + e.message;
4666
- } finally {
4667
- if (requestSeq === this.sessionStandaloneRequestSeq) {
4668
- this.sessionStandaloneLoading = false;
4669
- }
4670
- }
4671
- },
4672
-
4673
- async loadActiveSessionDetail() {
4674
- if (!this.activeSession) {
4675
- this.activeSessionMessages = [];
4676
- this.activeSessionDetailError = '';
4677
- this.activeSessionDetailClipped = false;
4678
- return;
4679
- }
4680
-
4681
- const requestSeq = ++this.sessionDetailRequestSeq;
4682
- this.sessionDetailLoading = true;
4683
- this.activeSessionDetailError = '';
4684
- try {
4685
- const res = await api('session-detail', {
4686
- source: this.activeSession.source,
4687
- sessionId: this.activeSession.sessionId,
4688
- filePath: this.activeSession.filePath,
4689
- messageLimit: 300
4690
- });
4691
-
4692
- if (requestSeq !== this.sessionDetailRequestSeq) {
4693
- return;
4694
- }
4695
-
4696
- if (res.error) {
4697
- this.activeSessionMessages = [];
4698
- this.activeSessionDetailClipped = false;
4699
- this.activeSessionDetailError = res.error;
4700
- return;
4701
- }
4702
-
4703
- this.activeSessionMessages = Array.isArray(res.messages) ? res.messages : [];
4704
- this.activeSessionDetailClipped = !!res.clipped;
4705
- if (this.activeSession) {
4706
- if (res.sourceLabel) {
4707
- this.activeSession.sourceLabel = res.sourceLabel;
4708
- }
4709
- if (res.sessionId) {
4710
- this.activeSession.sessionId = res.sessionId;
4711
- if (!this.activeSession.title) {
4712
- this.activeSession.title = res.sessionId;
4713
- }
4714
- }
4715
- if (res.filePath) {
4716
- this.activeSession.filePath = res.filePath;
4717
- }
4718
- }
4719
- if (res.updatedAt) {
4720
- this.activeSession.updatedAt = res.updatedAt;
4721
- }
4722
- if (res.cwd) {
4723
- this.activeSession.cwd = res.cwd;
4724
- }
4725
- if (Number.isFinite(res.totalMessages)) {
4726
- this.syncActiveSessionMessageCount(res.totalMessages);
4727
- }
4728
- } catch (e) {
4729
- if (requestSeq !== this.sessionDetailRequestSeq) {
4730
- return;
4731
- }
4732
- this.activeSessionMessages = [];
4733
- this.activeSessionDetailClipped = false;
4734
- this.activeSessionDetailError = '加载会话内容失败: ' + e.message;
4735
- } finally {
4736
- if (requestSeq === this.sessionDetailRequestSeq) {
4737
- this.sessionDetailLoading = false;
4738
- }
4739
- }
4740
- },
4741
-
4742
- downloadTextFile(fileName, content) {
4743
- const blob = new Blob([content], { type: 'text/markdown;charset=utf-8' });
4744
- const url = URL.createObjectURL(blob);
4745
- const link = document.createElement('a');
4746
- link.href = url;
4747
- link.download = fileName;
4748
- link.click();
4749
- URL.revokeObjectURL(url);
4750
- },
4751
-
4752
- async exportSession(session) {
4753
- const key = this.getSessionExportKey(session);
4754
- if (this.sessionExporting[key]) return;
4755
-
4756
- this.sessionExporting[key] = true;
4757
- try {
4758
- const res = await api('export-session', {
4759
- source: session.source,
4760
- sessionId: session.sessionId,
4761
- filePath: session.filePath
4762
- });
4763
- if (res.error) {
4764
- this.showMessage(res.error, 'error');
4765
- return;
4766
- }
4767
-
4768
- const fileName = res.fileName || `${session.source || 'session'}-${session.sessionId || Date.now()}.md`;
4769
- this.downloadTextFile(fileName, res.content || '');
4770
- if (res.truncated) {
4771
- const maxLabel = res.maxMessages === 'all' ? 'all' : res.maxMessages;
4772
- this.showMessage(`会话导出完成(已截断:最多 ${maxLabel} 条消息)`, 'info');
4773
- } else {
4774
- this.showMessage('会话导出完成', 'success');
4775
- }
4776
- } catch (e) {
4777
- this.showMessage('导出失败: ' + e.message, 'error');
4778
- } finally {
4779
- this.sessionExporting[key] = false;
4780
- }
4781
- },
4782
-
4783
- async switchProvider(name) {
4784
- this.currentProvider = name;
4785
- await this.loadModelsForProvider(name);
4786
- await this.openConfigTemplateEditor();
4787
- },
4788
-
4789
- async onModelChange() {
4790
- await this.openConfigTemplateEditor();
4791
- },
4792
-
4793
- async onServiceTierChange() {
4794
- await this.openConfigTemplateEditor();
4795
- },
4796
-
4797
- async runHealthCheck() {
4798
- this.healthCheckLoading = true;
4799
- this.healthCheckResult = null;
4800
- try {
4801
- const res = await api('config-health-check', {
4802
- remote: false
4803
- });
4804
- if (res && typeof res === 'object') {
4805
- const issues = Array.isArray(res.issues) ? [...res.issues] : [];
4806
- let remote = res.remote || null;
4807
- {
4808
- const providers = (this.providersList || [])
4809
- .filter(provider => provider && provider.name);
4810
- const tasks = providers.map(provider =>
4811
- this.runSpeedTest(provider.name, { silent: true })
4812
- .then(result => ({ name: provider.name, result }))
4813
- .catch(err => ({
4814
- name: provider.name,
4815
- result: { ok: false, error: err && err.message ? err.message : 'Speed test failed' }
4816
- }))
4817
- );
4818
- const pairs = await Promise.all(tasks);
4819
- const results = {};
4820
- for (const pair of pairs) {
4821
- results[pair.name] = pair.result || null;
4822
- const issue = this.buildSpeedTestIssue(pair.name, pair.result);
4823
- if (issue) issues.push(issue);
4824
- }
4825
- remote = {
4826
- type: 'speed-test',
4827
- results
4828
- };
4829
- }
4830
-
4831
- const ok = issues.length === 0;
4832
- this.healthCheckResult = {
4833
- ...res,
4834
- ok,
4835
- issues,
4836
- remote
4837
- };
4838
- if (ok) {
4839
- this.showMessage('健康检查通过', 'success');
4840
- }
4841
- } else {
4842
- this.healthCheckResult = null;
4843
- this.showMessage('健康检查失败:返回数据异常', 'error');
4844
- }
4845
- } catch (e) {
4846
- this.healthCheckResult = null;
4847
- this.showMessage('健康检查失败: ' + e.message, 'error');
4848
- } finally {
4849
- if (this.configMode === 'claude') {
4850
- try {
4851
- const entries = Object.entries(this.claudeConfigs || {});
4852
- await Promise.all(entries.map(([name, config]) => this.runClaudeSpeedTest(name, config)));
4853
- } catch (e) {}
4854
- }
4855
- this.healthCheckLoading = false;
4856
- }
4857
- },
4858
-
4859
- escapeTomlString(value) {
4860
- return String(value || '')
4861
- .replace(/\\/g, '\\\\')
4862
- .replace(/"/g, '\\"');
4863
- },
4864
-
4865
- async openConfigTemplateEditor(options = {}) {
4866
- try {
4867
- const res = await api('get-config-template', {
4868
- provider: this.currentProvider,
4869
- model: this.currentModel,
4870
- serviceTier: this.serviceTier
4871
- });
4872
- if (res.error) {
4873
- this.showMessage(res.error, 'error');
4874
- return;
4875
- }
4876
- let template = res.template || '';
4877
- const appendHint = typeof options.appendHint === 'string' ? options.appendHint.trim() : '';
4878
- const appendBlock = typeof options.appendBlock === 'string' ? options.appendBlock.trim() : '';
4879
- if (appendHint) {
4880
- template = `${template.trimEnd()}\n\n# -------------------------------\n# ${appendHint}\n# -------------------------------\n`;
4881
- }
4882
- if (appendBlock) {
4883
- template = `${template.trimEnd()}\n\n${appendBlock}\n`;
4884
- }
4885
- this.configTemplateContent = template;
4886
- this.showConfigTemplateModal = true;
4887
- } catch (e) {
4888
- this.showMessage('加载模板失败: ' + e.message, 'error');
4889
- }
4890
- },
4891
-
4892
- closeConfigTemplateModal() {
4893
- this.showConfigTemplateModal = false;
4894
- this.configTemplateContent = '';
4895
- },
4896
-
4897
- async applyConfigTemplate() {
4898
- if (!this.configTemplateContent || !this.configTemplateContent.trim()) {
4899
- this.showMessage('模板内容不能为空', 'error');
4900
- return;
4901
- }
4902
-
4903
- this.configTemplateApplying = true;
4904
- try {
4905
- const res = await api('apply-config-template', {
4906
- template: this.configTemplateContent
4907
- });
4908
- if (res.error) {
4909
- this.showMessage(res.error, 'error');
4910
- return;
4911
- }
4912
- this.showMessage('模板已应用到 config.toml', 'success');
4913
- this.closeConfigTemplateModal();
4914
- await this.loadAll();
4915
- } catch (e) {
4916
- this.showMessage('应用模板失败: ' + e.message, 'error');
4917
- } finally {
4918
- this.configTemplateApplying = false;
4919
- }
4920
- },
4921
-
4922
- async openAgentsEditor() {
4923
- this.setAgentsModalContext('codex');
4924
- this.agentsLoading = true;
4925
- try {
4926
- const res = await api('get-agents-file');
4927
- if (res.error) {
4928
- this.showMessage(res.error, 'error');
4929
- return;
4930
- }
4931
- this.agentsContent = res.content || '';
4932
- this.agentsPath = res.path || '';
4933
- this.agentsExists = !!res.exists;
4934
- this.agentsLineEnding = res.lineEnding === '\r\n' ? '\r\n' : '\n';
4935
- this.showAgentsModal = true;
4936
- } catch (e) {
4937
- this.showMessage('加载 AGENTS.md 失败: ' + e.message, 'error');
4938
- } finally {
4939
- this.agentsLoading = false;
4940
- }
4941
- },
4942
-
4943
- async openOpenclawAgentsEditor() {
4944
- this.setAgentsModalContext('openclaw');
4945
- this.agentsLoading = true;
4946
- try {
4947
- const res = await api('get-openclaw-agents-file');
4948
- if (res.error) {
4949
- this.showMessage(res.error, 'error');
4950
- return;
4951
- }
4952
- if (res.configError) {
4953
- this.showMessage(`OpenClaw 配置解析失败,已使用默认 Workspace:${res.configError}`, 'error');
4954
- }
4955
- this.agentsContent = res.content || '';
4956
- this.agentsPath = res.path || '';
4957
- this.agentsExists = !!res.exists;
4958
- this.agentsLineEnding = res.lineEnding === '\r\n' ? '\r\n' : '\n';
4959
- this.showAgentsModal = true;
4960
- } catch (e) {
4961
- this.showMessage('加载 OpenClaw AGENTS.md 失败: ' + e.message, 'error');
4962
- } finally {
4963
- this.agentsLoading = false;
4964
- }
4965
- },
4966
-
4967
- async openOpenclawWorkspaceEditor() {
4968
- const fileName = (this.openclawWorkspaceFileName || '').trim();
4969
- if (!fileName) {
4970
- this.showMessage('请输入工作区文件名', 'error');
4971
- return;
4972
- }
4973
- this.setAgentsModalContext('openclaw-workspace', { fileName });
4974
- this.agentsLoading = true;
4975
- try {
4976
- const res = await api('get-openclaw-workspace-file', { fileName });
4977
- if (res.error) {
4978
- this.showMessage(res.error, 'error');
4979
- return;
4980
- }
4981
- if (res.configError) {
4982
- this.showMessage(`OpenClaw 配置解析失败,已使用默认 Workspace:${res.configError}`, 'error');
4983
- }
4984
- this.agentsContent = res.content || '';
4985
- this.agentsPath = res.path || '';
4986
- this.agentsExists = !!res.exists;
4987
- this.agentsLineEnding = res.lineEnding === '\r\n' ? '\r\n' : '\n';
4988
- this.showAgentsModal = true;
4989
- } catch (e) {
4990
- this.showMessage('加载 OpenClaw 工作区文件失败: ' + e.message, 'error');
4991
- } finally {
4992
- this.agentsLoading = false;
4993
- }
4994
- },
4995
-
4996
- setAgentsModalContext(context, options = {}) {
4997
- if (context === 'openclaw-workspace') {
4998
- const fileName = (options.fileName || this.openclawWorkspaceFileName || 'AGENTS.md').trim();
4999
- this.agentsContext = 'openclaw-workspace';
5000
- this.agentsWorkspaceFileName = fileName;
5001
- this.agentsModalTitle = `OpenClaw 工作区文件: ${fileName}`;
5002
- this.agentsModalHint = `保存后会写入 OpenClaw Workspace 下的 ${fileName}。`;
5003
- return;
5004
- }
5005
- this.agentsContext = context === 'openclaw' ? 'openclaw' : 'codex';
5006
- if (this.agentsContext === 'openclaw') {
5007
- this.agentsModalTitle = 'OpenClaw AGENTS.md 编辑器';
5008
- this.agentsModalHint = '保存后会写入 OpenClaw Workspace 下的 AGENTS.md。';
5009
- } else {
5010
- this.agentsModalTitle = 'AGENTS.md 编辑器';
5011
- this.agentsModalHint = '保存后会写入目标 AGENTS.md(与 config.toml 同级)。';
5012
- }
5013
- this.agentsWorkspaceFileName = '';
5014
- },
5015
-
5016
- closeAgentsModal() {
5017
- this.showAgentsModal = false;
5018
- this.agentsContent = '';
5019
- this.agentsPath = '';
5020
- this.agentsExists = false;
5021
- this.agentsLineEnding = '\n';
5022
- this.agentsSaving = false;
5023
- this.agentsWorkspaceFileName = '';
5024
- this.setAgentsModalContext('codex');
5025
- },
5026
-
5027
- async applyAgentsContent() {
5028
- this.agentsSaving = true;
5029
- try {
5030
- let action = 'apply-agents-file';
5031
- const params = {
5032
- content: this.agentsContent,
5033
- lineEnding: this.agentsLineEnding
5034
- };
5035
- if (this.agentsContext === 'openclaw') {
5036
- action = 'apply-openclaw-agents-file';
5037
- } else if (this.agentsContext === 'openclaw-workspace') {
5038
- action = 'apply-openclaw-workspace-file';
5039
- params.fileName = this.agentsWorkspaceFileName;
5040
- }
5041
- const res = await api(action, params);
5042
- if (res.error) {
5043
- this.showMessage(res.error, 'error');
5044
- return;
5045
- }
5046
- const successLabel = this.agentsContext === 'openclaw-workspace'
5047
- ? `工作区文件已保存${this.agentsWorkspaceFileName ? `: ${this.agentsWorkspaceFileName}` : ''}`
5048
- : (this.agentsContext === 'openclaw' ? 'OpenClaw AGENTS.md 已保存' : 'AGENTS.md 已保存');
5049
- this.showMessage(successLabel, 'success');
5050
- this.closeAgentsModal();
5051
- } catch (e) {
5052
- this.showMessage('保存文件失败: ' + e.message, 'error');
5053
- } finally {
5054
- this.agentsSaving = false;
5055
- }
5056
- },
5057
-
5058
- async addProvider() {
5059
- if (!this.newProvider.name || !this.newProvider.url) {
5060
- return this.showMessage('名称和URL必填', 'error');
5061
- }
5062
- const name = this.newProvider.name.trim();
5063
- if (!name) {
5064
- return this.showMessage('名称不能为空', 'error');
5065
- }
5066
- if (this.providersList.some(item => item.name === name)) {
5067
- return this.showMessage('提供商已存在', 'error');
5068
- }
5069
-
5070
- const safeName = this.escapeTomlString(name);
5071
- const safeUrl = this.escapeTomlString(this.newProvider.url.trim());
5072
- const safeKey = this.escapeTomlString(this.newProvider.key || '');
5073
- const newProviderBlock = `[model_providers.${safeName}]\nname = "${safeName}"\nbase_url = "${safeUrl}"\nwire_api = "responses"\nrequires_openai_auth = false\npreferred_auth_method = "${safeKey}"\nrequest_max_retries = 4\nstream_max_retries = 10\nstream_idle_timeout_ms = 300000`;
5074
-
5075
- this.currentProvider = name;
5076
- this.showMessage('已生成新增模板,请确认后应用', 'info');
5077
- this.closeAddModal();
5078
- await this.openConfigTemplateEditor({
5079
- appendHint: `新增 provider: ${name}(请检查字段后应用)`,
5080
- appendBlock: newProviderBlock
5081
- });
5082
- },
5083
-
5084
- async deleteProvider(name) {
5085
- if (!confirm(`确定删除提供商 "${name}"?`)) return;
5086
- this.showMessage('请在模板中手动删除该 provider 配置块后应用', 'info');
5087
- await this.openConfigTemplateEditor({
5088
- appendHint: `请手动删除 [model_providers.${name}] 配置块,并确认 model_provider 指向有效 provider`
5089
- });
5090
- },
5091
-
5092
- openEditModal(provider) {
5093
- this.editingProvider = {
5094
- name: provider.name,
5095
- url: provider.url || '',
5096
- key: ''
5097
- };
5098
- this.showEditModal = true;
5099
- },
5100
-
5101
- async updateProvider() {
5102
- if (!this.editingProvider.url) {
5103
- return this.showMessage('URL 必填', 'error');
5104
- }
5105
-
5106
- const name = this.editingProvider.name;
5107
- const safeUrl = this.escapeTomlString(this.editingProvider.url.trim());
5108
- const safeKey = this.escapeTomlString(this.editingProvider.key || '');
5109
- this.closeEditModal();
5110
- this.showMessage('已生成更新模板,请确认后应用', 'info');
5111
- await this.openConfigTemplateEditor({
5112
- appendHint: `请将 [model_providers.${name}] 中 base_url 更新为 ${safeUrl}${safeKey ? ',并更新 preferred_auth_method' : ''}`
5113
- });
5114
- },
5115
-
5116
- closeEditModal() {
5117
- this.showEditModal = false;
5118
- this.editingProvider = { name: '', url: '', key: '' };
5119
- },
5120
-
5121
- async addModel() {
5122
- if (!this.newModelName || !this.newModelName.trim()) {
5123
- return this.showMessage('请输入模型名称', 'error');
5124
- }
5125
- const res = await api('add-model', { model: this.newModelName.trim() });
5126
- if (res.error) {
5127
- this.showMessage(res.error, 'error');
5128
- } else {
5129
- this.showMessage('已添加', 'success');
5130
- this.closeModelModal();
5131
- await this.loadAll();
5132
- }
5133
- },
5134
-
5135
- async removeModel(model) {
5136
- if (!confirm(`确定删除模型 "${model}"?`)) return;
5137
- const res = await api('delete-model', { model });
5138
- if (res.error) {
5139
- this.showMessage(res.error, 'error');
5140
- } else {
5141
- this.showMessage('已删除', 'success');
5142
- await this.loadAll();
5143
- }
5144
- },
5145
-
5146
- closeAddModal() {
5147
- this.showAddModal = false;
5148
- this.newProvider = { name: '', url: '', key: '' };
5149
- },
5150
-
5151
- closeModelModal() {
5152
- this.showModelModal = false;
5153
- this.newModelName = '';
5154
- },
5155
-
5156
- formatKey(key) {
5157
- if (!key) return '(未设置)';
5158
- if (key.length > 10) {
5159
- return key.substring(0, 3) + '****' + key.substring(key.length - 3);
5160
- }
5161
- return '****';
5162
- },
5163
-
5164
- displayApiKey(configName) {
5165
- const key = this.claudeConfigs[configName]?.apiKey;
5166
- return this.formatKey(key);
5167
- },
5168
-
5169
- switchClaudeConfig(name) {
5170
- this.currentClaudeConfig = name;
5171
- this.refreshClaudeModelContext();
5172
- },
5173
-
5174
- onClaudeModelChange() {
5175
- const name = this.currentClaudeConfig;
5176
- if (!name) {
5177
- this.showMessage('请先选择配置', 'error');
5178
- return;
5179
- }
5180
- const model = (this.currentClaudeModel || '').trim();
5181
- if (!model) {
5182
- this.showMessage('请输入模型', 'error');
5183
- return;
5184
- }
5185
- const existing = this.claudeConfigs[name] || {};
5186
- this.currentClaudeModel = model;
5187
- this.claudeConfigs[name] = {
5188
- apiKey: existing.apiKey || '',
5189
- baseUrl: existing.baseUrl || '',
5190
- model: model,
5191
- hasKey: !!existing.apiKey
5192
- };
5193
- this.saveClaudeConfigs();
5194
- this.updateClaudeModelsCurrent();
5195
- if (!this.claudeConfigs[name].apiKey) {
5196
- this.showMessage('该配置未设置 API Key,请先编辑', 'error');
5197
- return;
5198
- }
5199
- this.applyClaudeConfig(name);
5200
- },
5201
-
5202
- saveClaudeConfigs() {
5203
- localStorage.setItem('claudeConfigs', JSON.stringify(this.claudeConfigs));
5204
- },
5205
-
5206
- openEditConfigModal(name) {
5207
- const config = this.claudeConfigs[name];
5208
- this.editingConfig = {
5209
- name: name,
5210
- apiKey: config.apiKey || '',
5211
- baseUrl: config.baseUrl || '',
5212
- model: config.model || ''
5213
- };
5214
- this.showEditConfigModal = true;
5215
- },
5216
-
5217
- updateConfig() {
5218
- const name = this.editingConfig.name;
5219
- this.claudeConfigs[name] = {
5220
- apiKey: this.editingConfig.apiKey,
5221
- baseUrl: this.editingConfig.baseUrl,
5222
- model: this.editingConfig.model,
5223
- hasKey: !!this.editingConfig.apiKey
5224
- };
5225
- this.saveClaudeConfigs();
5226
- this.showMessage('配置已更新', 'success');
5227
- this.closeEditConfigModal();
5228
- if (name === this.currentClaudeConfig) {
5229
- this.refreshClaudeModelContext();
5230
- }
5231
- },
5232
-
5233
- closeEditConfigModal() {
5234
- this.showEditConfigModal = false;
5235
- this.editingConfig = { name: '', apiKey: '', baseUrl: '', model: '' };
5236
- },
5237
-
5238
- async saveAndApplyConfig() {
5239
- const name = this.editingConfig.name;
5240
- this.claudeConfigs[name] = {
5241
- apiKey: this.editingConfig.apiKey,
5242
- baseUrl: this.editingConfig.baseUrl,
5243
- model: this.editingConfig.model,
5244
- hasKey: !!this.editingConfig.apiKey
5245
- };
5246
- this.saveClaudeConfigs();
5247
-
5248
- const config = this.claudeConfigs[name];
5249
- if (!config.apiKey) {
5250
- this.showMessage('已保存,未应用:请先输入 API Key', 'info');
5251
- this.closeEditConfigModal();
5252
- if (name === this.currentClaudeConfig) {
5253
- this.refreshClaudeModelContext();
5254
- }
5255
- return;
5256
- }
5257
-
5258
- const res = await api('apply-claude-config', { config });
5259
- if (res.error || res.success === false) {
5260
- this.showMessage(res.error || '应用 Claude 配置失败', 'error');
5261
- } else {
5262
- const targetTip = res.targetPath ? `(${res.targetPath})` : '';
5263
- this.showMessage(`已保存并应用到 Claude 配置${targetTip}`, 'success');
5264
- this.closeEditConfigModal();
5265
- if (name === this.currentClaudeConfig) {
5266
- this.refreshClaudeModelContext();
5267
- }
5268
- }
5269
- },
5270
-
5271
- addClaudeConfig() {
5272
- if (!this.newClaudeConfig.name || !this.newClaudeConfig.name.trim()) {
5273
- return this.showMessage('请输入配置名称', 'error');
5274
- }
5275
- const name = this.newClaudeConfig.name.trim();
5276
- if (this.claudeConfigs[name]) {
5277
- return this.showMessage('配置名称已存在', 'error');
5278
- }
5279
- const duplicateName = this.findDuplicateClaudeConfigName(this.newClaudeConfig);
5280
- if (duplicateName) {
5281
- return this.showMessage('已存在相同配置,已忽略添加', 'info');
5282
- }
5283
-
5284
- this.claudeConfigs[name] = {
5285
- apiKey: this.newClaudeConfig.apiKey,
5286
- baseUrl: this.newClaudeConfig.baseUrl,
5287
- model: this.newClaudeConfig.model,
5288
- hasKey: !!this.newClaudeConfig.apiKey
5289
- };
5290
-
5291
- this.currentClaudeConfig = name;
5292
- this.saveClaudeConfigs();
5293
- this.showMessage('配置已添加', 'success');
5294
- this.closeClaudeConfigModal();
5295
- this.refreshClaudeModelContext();
5296
- },
5297
-
5298
- deleteClaudeConfig(name) {
5299
- if (Object.keys(this.claudeConfigs).length <= 1) {
5300
- return this.showMessage('至少保留一个配置', 'error');
5301
- }
5302
-
5303
- if (!confirm(`确定删除配置 "${name}"?`)) return;
5304
-
5305
- delete this.claudeConfigs[name];
5306
- if (this.currentClaudeConfig === name) {
5307
- this.currentClaudeConfig = Object.keys(this.claudeConfigs)[0];
5308
- }
5309
- this.saveClaudeConfigs();
5310
- this.showMessage('配置已删除', 'success');
5311
- this.refreshClaudeModelContext();
5312
- },
5313
-
5314
- async applyClaudeConfig(name) {
5315
- this.currentClaudeConfig = name;
5316
- this.refreshClaudeModelContext();
5317
- const config = this.claudeConfigs[name];
5318
-
5319
- if (!config.apiKey) {
5320
- return this.showMessage('该配置未设置 API Key,请先编辑', 'error');
5321
- }
5322
-
5323
- const res = await api('apply-claude-config', { config });
5324
- if (res.error || res.success === false) {
5325
- this.showMessage(res.error || '应用 Claude 配置失败', 'error');
5326
- } else {
5327
- const targetTip = res.targetPath ? `(${res.targetPath})` : '';
5328
- this.showMessage(`已应用配置到 Claude 设置: ${name}${targetTip}`, 'success');
5329
- }
5330
- },
5331
-
5332
- closeClaudeConfigModal() {
5333
- this.showClaudeConfigModal = false;
5334
- this.newClaudeConfig = {
5335
- name: '',
5336
- apiKey: '',
5337
- baseUrl: 'https://open.bigmodel.cn/api/anthropic',
5338
- model: 'glm-4.7'
5339
- };
5340
- },
5341
-
5342
- getOpenclawParser() {
5343
- if (window.JSON5 && typeof window.JSON5.parse === 'function' && typeof window.JSON5.stringify === 'function') {
5344
- return {
5345
- parse: window.JSON5.parse,
5346
- stringify: window.JSON5.stringify
5347
- };
5348
- }
5349
- return {
5350
- parse: JSON.parse,
5351
- stringify: JSON.stringify
5352
- };
5353
- },
5354
-
5355
- parseOpenclawContent(content, options = {}) {
5356
- const allowEmpty = !!options.allowEmpty;
5357
- const raw = typeof content === 'string' ? content.trim() : '';
5358
- if (!raw) {
5359
- if (allowEmpty) {
5360
- return { ok: true, data: {} };
5361
- }
5362
- return { ok: false, error: '配置内容为空' };
5363
- }
5364
- try {
5365
- const parser = this.getOpenclawParser();
5366
- const data = parser.parse(raw);
5367
- if (!data || typeof data !== 'object' || Array.isArray(data)) {
5368
- return { ok: false, error: '配置格式错误(根节点必须是对象)' };
5369
- }
5370
- return { ok: true, data };
5371
- } catch (e) {
5372
- return { ok: false, error: e.message || '解析失败' };
5373
- }
5374
- },
5375
-
5376
- stringifyOpenclawConfig(data) {
5377
- const parser = this.getOpenclawParser();
5378
- try {
5379
- return parser.stringify(data, null, 2);
5380
- } catch (e) {
5381
- return JSON.stringify(data, null, 2);
5382
- }
5383
- },
5384
-
5385
- resetOpenclawStructured() {
5386
- this.openclawStructured = {
5387
- agentPrimary: '',
5388
- agentFallbacks: [''],
5389
- workspace: '',
5390
- timeout: '',
5391
- contextTokens: '',
5392
- maxConcurrent: '',
5393
- envItems: [{ key: '', value: '', show: false }],
5394
- toolsProfile: 'default',
5395
- toolsAllow: [''],
5396
- toolsDeny: ['']
5397
- };
5398
- this.openclawAgentsList = [];
5399
- this.openclawProviders = [];
5400
- this.openclawMissingProviders = [];
5401
- },
5402
-
5403
- getOpenclawQuickDefaults() {
5404
- return {
5405
- providerName: '',
5406
- baseUrl: '',
5407
- apiKey: '',
5408
- apiType: 'openai-responses',
5409
- modelId: '',
5410
- modelName: '',
5411
- contextWindow: '',
5412
- maxTokens: '',
5413
- setPrimary: true,
5414
- overrideProvider: true,
5415
- overrideModels: true,
5416
- showKey: false
5417
- };
5418
- },
5419
-
5420
- resetOpenclawQuick() {
5421
- this.openclawQuick = this.getOpenclawQuickDefaults();
5422
- },
5423
-
5424
- toggleOpenclawQuickKey() {
5425
- this.openclawQuick.showKey = !this.openclawQuick.showKey;
5426
- },
5427
-
5428
- fillOpenclawQuickFromConfig(config) {
5429
- const defaults = this.getOpenclawQuickDefaults();
5430
- if (!config || typeof config !== 'object' || Array.isArray(config)) {
5431
- this.openclawQuick = defaults;
5432
- return;
5433
- }
5434
-
5435
- const agentDefaults = config.agents && typeof config.agents === 'object' && !Array.isArray(config.agents)
5436
- && config.agents.defaults && typeof config.agents.defaults === 'object' && !Array.isArray(config.agents.defaults)
5437
- ? config.agents.defaults
5438
- : {};
5439
- const modelConfig = agentDefaults.model;
5440
- const legacyAgent = config.agent && typeof config.agent === 'object' && !Array.isArray(config.agent)
5441
- ? config.agent
5442
- : {};
5443
-
5444
- let primaryRef = '';
5445
- if (modelConfig && typeof modelConfig === 'object' && !Array.isArray(modelConfig) && typeof modelConfig.primary === 'string') {
5446
- primaryRef = modelConfig.primary;
5447
- } else if (typeof modelConfig === 'string') {
5448
- primaryRef = modelConfig;
5449
- }
5450
- if (!primaryRef) {
5451
- if (typeof legacyAgent.model === 'string') {
5452
- primaryRef = legacyAgent.model;
5453
- } else if (legacyAgent.model && typeof legacyAgent.model === 'object' && typeof legacyAgent.model.primary === 'string') {
5454
- primaryRef = legacyAgent.model.primary;
5455
- }
5456
- }
5457
-
5458
- let providerName = '';
5459
- let modelId = '';
5460
- if (primaryRef) {
5461
- const parts = primaryRef.split('/');
5462
- if (parts.length >= 2) {
5463
- providerName = parts.shift().trim();
5464
- modelId = parts.join('/').trim();
5465
- }
5466
- }
5467
-
5468
- const providers = config.models && typeof config.models === 'object' && !Array.isArray(config.models)
5469
- && config.models.providers && typeof config.models.providers === 'object' && !Array.isArray(config.models.providers)
5470
- ? config.models.providers
5471
- : null;
5472
- let providerConfig = providerName && providers ? providers[providerName] : null;
5473
- if (!providerName && providers) {
5474
- const providerKeys = Object.keys(providers);
5475
- if (providerKeys.length === 1) {
5476
- providerName = providerKeys[0];
5477
- providerConfig = providers[providerName];
5478
- }
5479
- }
5480
-
5481
- let modelEntry = null;
5482
- if (providerConfig && typeof providerConfig === 'object' && Array.isArray(providerConfig.models)) {
5483
- if (modelId) {
5484
- modelEntry = providerConfig.models.find(item => item && item.id === modelId);
5485
- }
5486
- if (!modelEntry && providerConfig.models.length === 1) {
5487
- modelEntry = providerConfig.models[0];
5488
- if (!modelId && modelEntry && typeof modelEntry.id === 'string') {
5489
- modelId = modelEntry.id;
5490
- }
5491
- }
5492
- }
5493
-
5494
- const baseUrl = providerConfig && typeof providerConfig === 'object' && typeof providerConfig.baseUrl === 'string'
5495
- ? providerConfig.baseUrl
5496
- : '';
5497
- const apiKey = providerConfig && typeof providerConfig === 'object' && typeof providerConfig.apiKey === 'string'
5498
- ? providerConfig.apiKey
5499
- : '';
5500
- const apiType = providerConfig && typeof providerConfig === 'object' && typeof providerConfig.api === 'string'
5501
- ? providerConfig.api
5502
- : defaults.apiType;
5503
-
5504
- this.openclawQuick = {
5505
- ...defaults,
5506
- providerName,
5507
- baseUrl,
5508
- apiKey,
5509
- apiType,
5510
- modelId: modelId || '',
5511
- modelName: modelEntry && typeof modelEntry.name === 'string' ? modelEntry.name : '',
5512
- contextWindow: modelEntry && typeof modelEntry.contextWindow === 'number'
5513
- ? String(modelEntry.contextWindow)
5514
- : '',
5515
- maxTokens: modelEntry && typeof modelEntry.maxTokens === 'number'
5516
- ? String(modelEntry.maxTokens)
5517
- : ''
5518
- };
5519
- },
5520
-
5521
- syncOpenclawQuickFromText(options = {}) {
5522
- const silent = !!options.silent;
5523
- const parsed = this.parseOpenclawContent(this.openclawEditing.content, { allowEmpty: true });
5524
- if (!parsed.ok) {
5525
- this.resetOpenclawQuick();
5526
- if (!silent) {
5527
- this.showMessage('解析 OpenClaw 配置失败: ' + parsed.error, 'error');
5528
- }
5529
- return false;
5530
- }
5531
- this.fillOpenclawQuickFromConfig(parsed.data);
5532
- if (!silent) {
5533
- this.showMessage('已从编辑器读取快速配置', 'success');
5534
- }
5535
- return true;
5536
- },
5537
-
5538
- mergeOpenclawModelEntry(existing, incoming, overwrite = false) {
5539
- if (!existing || typeof existing !== 'object' || Array.isArray(existing)) {
5540
- return { ...incoming };
5541
- }
5542
- if (overwrite) {
5543
- return { ...incoming };
5544
- }
5545
- const merged = { ...existing };
5546
- for (const [key, value] of Object.entries(incoming || {})) {
5547
- if (merged[key] === undefined || merged[key] === null || merged[key] === '') {
5548
- merged[key] = value;
5549
- }
5550
- }
5551
- return merged;
5552
- },
5553
-
5554
- fillOpenclawStructured(config) {
5555
- const defaults = config && config.agents && typeof config.agents === 'object' && !Array.isArray(config.agents)
5556
- && config.agents.defaults && typeof config.agents.defaults === 'object' && !Array.isArray(config.agents.defaults)
5557
- ? config.agents.defaults
5558
- : {};
5559
- const model = defaults.model && typeof defaults.model === 'object' && !Array.isArray(defaults.model)
5560
- ? defaults.model
5561
- : {};
5562
- const legacyAgent = config && config.agent && typeof config.agent === 'object' && !Array.isArray(config.agent)
5563
- ? config.agent
5564
- : {};
5565
- const fallbackList = Array.isArray(model.fallbacks)
5566
- ? model.fallbacks.filter(item => typeof item === 'string' && item.trim()).map(item => item.trim())
5567
- : [];
5568
- const env = config && config.env && typeof config.env === 'object' && !Array.isArray(config.env)
5569
- ? config.env
5570
- : {};
5571
- const envItems = Object.entries(env).map(([key, value]) => ({
5572
- key,
5573
- value: value == null ? '' : String(value),
5574
- show: false
5575
- }));
5576
- const tools = config && config.tools && typeof config.tools === 'object' && !Array.isArray(config.tools)
5577
- ? config.tools
5578
- : {};
5579
-
5580
- let primary = typeof model.primary === 'string' ? model.primary : '';
5581
- if (!primary) {
5582
- if (typeof legacyAgent.model === 'string') {
5583
- primary = legacyAgent.model;
5584
- } else if (legacyAgent.model && typeof legacyAgent.model === 'object' && typeof legacyAgent.model.primary === 'string') {
5585
- primary = legacyAgent.model.primary;
5586
- }
5587
- }
5588
-
5589
- this.openclawStructured = {
5590
- agentPrimary: primary,
5591
- agentFallbacks: fallbackList.length ? fallbackList : [''],
5592
- workspace: typeof defaults.workspace === 'string' ? defaults.workspace : '',
5593
- timeout: typeof defaults.timeout === 'number' && Number.isFinite(defaults.timeout)
5594
- ? String(defaults.timeout)
5595
- : '',
5596
- contextTokens: typeof defaults.contextTokens === 'number' && Number.isFinite(defaults.contextTokens)
5597
- ? String(defaults.contextTokens)
5598
- : '',
5599
- maxConcurrent: typeof defaults.maxConcurrent === 'number' && Number.isFinite(defaults.maxConcurrent)
5600
- ? String(defaults.maxConcurrent)
5601
- : '',
5602
- envItems: envItems.length ? envItems : [{ key: '', value: '', show: false }],
5603
- toolsProfile: typeof tools.profile === 'string' && tools.profile.trim() ? tools.profile : 'default',
5604
- toolsAllow: Array.isArray(tools.allow) && tools.allow.length
5605
- ? tools.allow.filter(item => typeof item === 'string' && item.trim()).map(item => item.trim())
5606
- : [''],
5607
- toolsDeny: Array.isArray(tools.deny) && tools.deny.length
5608
- ? tools.deny.filter(item => typeof item === 'string' && item.trim()).map(item => item.trim())
5609
- : ['']
5610
- };
5611
- },
5612
-
5613
- syncOpenclawStructuredFromText(options = {}) {
5614
- const silent = !!options.silent;
5615
- const parsed = this.parseOpenclawContent(this.openclawEditing.content, { allowEmpty: true });
5616
- if (!parsed.ok) {
5617
- this.resetOpenclawStructured();
5618
- this.resetOpenclawQuick();
5619
- if (!silent) {
5620
- this.showMessage('解析 OpenClaw 配置失败: ' + parsed.error, 'error');
5621
- }
5622
- return false;
5623
- }
5624
- this.fillOpenclawStructured(parsed.data);
5625
- this.fillOpenclawQuickFromConfig(parsed.data);
5626
- this.refreshOpenclawProviders(parsed.data);
5627
- this.refreshOpenclawAgentsList(parsed.data);
5628
- if (!silent) {
5629
- this.showMessage('已从文本刷新结构化配置', 'success');
5630
- }
5631
- return true;
5632
- },
5633
-
5634
- getOpenclawActiveProviders(config) {
5635
- const active = new Set();
5636
- const addProvider = (ref) => {
5637
- if (typeof ref !== 'string') return;
5638
- const text = ref.trim();
5639
- if (!text) return;
5640
- const parts = text.split('/');
5641
- if (parts.length < 2) return;
5642
- const provider = parts[0].trim();
5643
- if (provider) active.add(provider);
5644
- };
5645
- const defaults = config && config.agents && config.agents.defaults
5646
- ? config.agents.defaults
5647
- : {};
5648
- const model = defaults && defaults.model;
5649
- if (model && typeof model === 'object' && !Array.isArray(model)) {
5650
- addProvider(model.primary);
5651
- if (Array.isArray(model.fallbacks)) {
5652
- for (const item of model.fallbacks) {
5653
- addProvider(item);
5654
- }
5655
- }
5656
- } else if (typeof model === 'string') {
5657
- addProvider(model);
5658
- }
5659
- const modelsDefaults = config && config.models && config.models.defaults
5660
- ? config.models.defaults
5661
- : {};
5662
- if (modelsDefaults && typeof modelsDefaults.provider === 'string' && modelsDefaults.provider.trim()) {
5663
- active.add(modelsDefaults.provider.trim());
5664
- }
5665
- if (modelsDefaults && typeof modelsDefaults.model === 'string') {
5666
- addProvider(modelsDefaults.model);
5667
- }
5668
- return active;
5669
- },
5670
-
5671
- maskProviderValue(value) {
5672
- const text = value == null ? '' : String(value);
5673
- if (!text) return '****';
5674
- if (text.length <= 6) return '****';
5675
- return `${text.slice(0, 3)}****${text.slice(-3)}`;
5676
- },
5677
-
5678
- formatProviderValue(key, value) {
5679
- if (typeof value === 'undefined' || value === null) {
5680
- return '';
5681
- }
5682
- let text = '';
5683
- if (typeof value === 'string') {
5684
- text = value;
5685
- } else if (typeof value === 'number' || typeof value === 'boolean') {
5686
- text = String(value);
5687
- } else {
5688
- try {
5689
- text = JSON.stringify(value);
5690
- } catch (_) {
5691
- text = String(value);
5692
- }
5693
- }
5694
- if (!text) return '';
5695
- if (/key|token|secret|password/i.test(key)) {
5696
- return this.maskProviderValue(text);
5697
- }
5698
- if (text.length > 160) {
5699
- return `${text.slice(0, 157)}...`;
5700
- }
5701
- return text;
5702
- },
5703
-
5704
- collectOpenclawProviders(source, providerMap, activeProviders, entries) {
5705
- if (!providerMap || typeof providerMap !== 'object' || Array.isArray(providerMap)) {
5706
- return;
5707
- }
5708
- const keys = Object.keys(providerMap).sort();
5709
- for (const key of keys) {
5710
- const value = providerMap[key];
5711
- const fields = [];
5712
- if (value && typeof value === 'object' && !Array.isArray(value)) {
5713
- const fieldKeys = Object.keys(value).sort();
5714
- for (const fieldKey of fieldKeys) {
5715
- const fieldValue = this.formatProviderValue(fieldKey, value[fieldKey]);
5716
- if (fieldValue === '') continue;
5717
- fields.push({ key: fieldKey, value: fieldValue });
5718
- }
5719
- } else {
5720
- const fieldValue = this.formatProviderValue('value', value);
5721
- if (fieldValue !== '') {
5722
- fields.push({ key: 'value', value: fieldValue });
5723
- }
5724
- }
5725
- entries.push({
5726
- key,
5727
- source,
5728
- fields,
5729
- isActive: activeProviders.has(key)
5730
- });
5731
- }
5732
- },
5733
-
5734
- refreshOpenclawProviders(config) {
5735
- const activeProviders = this.getOpenclawActiveProviders(config || {});
5736
- const entries = [];
5737
- const modelsProviders = config && config.models ? config.models.providers : null;
5738
- const rootProviders = config && config.providers ? config.providers : null;
5739
- this.collectOpenclawProviders('models.providers', modelsProviders, activeProviders, entries);
5740
- this.collectOpenclawProviders('providers', rootProviders, activeProviders, entries);
5741
- const existing = new Set(entries.map(item => item.key));
5742
- const missing = [];
5743
- for (const provider of activeProviders) {
5744
- if (!existing.has(provider)) {
5745
- missing.push(provider);
5746
- }
5747
- }
5748
- this.openclawProviders = entries;
5749
- this.openclawMissingProviders = missing;
5750
- },
5751
-
5752
- refreshOpenclawAgentsList(config) {
5753
- const list = config && config.agents && typeof config.agents === 'object' && !Array.isArray(config.agents)
5754
- ? config.agents.list
5755
- : null;
5756
- if (!Array.isArray(list)) {
5757
- this.openclawAgentsList = [];
5758
- return;
5759
- }
5760
- const entries = [];
5761
- list.forEach((item, index) => {
5762
- if (!item || typeof item !== 'object') return;
5763
- const id = typeof item.id === 'string' && item.id.trim() ? item.id.trim() : `agent-${index + 1}`;
5764
- const identity = item.identity && typeof item.identity === 'object' && !Array.isArray(item.identity)
5765
- ? item.identity
5766
- : {};
5767
- const name = typeof identity.name === 'string' && identity.name.trim()
5768
- ? identity.name.trim()
5769
- : id;
5770
- entries.push({
5771
- key: `${id}-${index}`,
5772
- id,
5773
- name,
5774
- theme: typeof identity.theme === 'string' ? identity.theme : '',
5775
- emoji: typeof identity.emoji === 'string' ? identity.emoji : '',
5776
- avatar: typeof identity.avatar === 'string' ? identity.avatar : ''
5777
- });
5778
- });
5779
- this.openclawAgentsList = entries;
5780
- },
5781
-
5782
- normalizeStringList(list) {
5783
- if (!Array.isArray(list)) return [];
5784
- const result = [];
5785
- const seen = new Set();
5786
- for (const item of list) {
5787
- const value = typeof item === 'string' ? item.trim() : String(item || '').trim();
5788
- if (!value) continue;
5789
- const key = value;
5790
- if (seen.has(key)) continue;
5791
- seen.add(key);
5792
- result.push(value);
5793
- }
5794
- return result;
5795
- },
5796
-
5797
- normalizeEnvItems(items) {
5798
- if (!Array.isArray(items)) {
5799
- return { ok: true, items: {} };
5800
- }
5801
- const output = {};
5802
- const seen = new Set();
5803
- for (const item of items) {
5804
- const key = item && typeof item.key === 'string' ? item.key.trim() : '';
5805
- if (!key) continue;
5806
- if (seen.has(key)) {
5807
- return { ok: false, error: `环境变量重复: ${key}` };
5808
- }
5809
- seen.add(key);
5810
- const value = item && typeof item.value !== 'undefined' ? String(item.value) : '';
5811
- output[key] = value;
5812
- }
5813
- return { ok: true, items: output };
5814
- },
5815
-
5816
- parseOptionalNumber(value, label) {
5817
- const text = typeof value === 'string' ? value.trim() : String(value || '').trim();
5818
- if (!text) {
5819
- return { ok: true, value: null };
5820
- }
5821
- const num = Number(text);
5822
- if (!Number.isFinite(num) || num < 0) {
5823
- return { ok: false, error: `${label} 请输入有效数字` };
5824
- }
5825
- return { ok: true, value: num };
5826
- },
5827
-
5828
- applyOpenclawStructuredToText() {
5829
- const parsed = this.parseOpenclawContent(this.openclawEditing.content, { allowEmpty: true });
5830
- if (!parsed.ok) {
5831
- this.showMessage('解析 OpenClaw 配置失败: ' + parsed.error, 'error');
5832
- return;
5833
- }
5834
-
5835
- const config = parsed.data;
5836
- const agents = config.agents && typeof config.agents === 'object' && !Array.isArray(config.agents)
5837
- ? config.agents
5838
- : {};
5839
- const defaults = agents.defaults && typeof agents.defaults === 'object' && !Array.isArray(agents.defaults)
5840
- ? agents.defaults
5841
- : {};
5842
- const model = defaults.model && typeof defaults.model === 'object' && !Array.isArray(defaults.model)
5843
- ? defaults.model
5844
- : {};
5845
-
5846
- const primary = (this.openclawStructured.agentPrimary || '').trim();
5847
- const fallbacks = this.normalizeStringList(this.openclawStructured.agentFallbacks);
5848
- if (primary) {
5849
- model.primary = primary;
5850
- }
5851
- if (fallbacks.length) {
5852
- model.fallbacks = fallbacks;
5853
- }
5854
- if (primary || fallbacks.length) {
5855
- defaults.model = model;
5856
- }
5857
- if (primary && config.agent && typeof config.agent === 'object' && !Array.isArray(config.agent)) {
5858
- config.agent.model = primary;
5859
- }
5860
-
5861
- const workspace = (this.openclawStructured.workspace || '').trim();
5862
- if (workspace) {
5863
- defaults.workspace = workspace;
5864
- }
5865
-
5866
- const timeout = this.parseOptionalNumber(this.openclawStructured.timeout, 'Timeout');
5867
- if (!timeout.ok) {
5868
- this.showMessage(timeout.error, 'error');
5869
- return;
5870
- }
5871
- if (timeout.value !== null) {
5872
- defaults.timeout = timeout.value;
5873
- }
5874
-
5875
- const contextTokens = this.parseOptionalNumber(this.openclawStructured.contextTokens, 'Context Tokens');
5876
- if (!contextTokens.ok) {
5877
- this.showMessage(contextTokens.error, 'error');
5878
- return;
5879
- }
5880
- if (contextTokens.value !== null) {
5881
- defaults.contextTokens = contextTokens.value;
5882
- }
5883
-
5884
- const maxConcurrent = this.parseOptionalNumber(this.openclawStructured.maxConcurrent, 'Max Concurrent');
5885
- if (!maxConcurrent.ok) {
5886
- this.showMessage(maxConcurrent.error, 'error');
5887
- return;
5888
- }
5889
- if (maxConcurrent.value !== null) {
5890
- defaults.maxConcurrent = maxConcurrent.value;
5891
- }
5892
-
5893
- if (Object.keys(defaults).length > 0) {
5894
- config.agents = agents;
5895
- config.agents.defaults = defaults;
5896
- }
5897
-
5898
- const envResult = this.normalizeEnvItems(this.openclawStructured.envItems);
5899
- if (!envResult.ok) {
5900
- this.showMessage(envResult.error, 'error');
5901
- return;
5902
- }
5903
- if (Object.keys(envResult.items).length > 0) {
5904
- config.env = envResult.items;
5905
- } else if (config.env) {
5906
- delete config.env;
5907
- }
5908
-
5909
- const profile = (this.openclawStructured.toolsProfile || '').trim();
5910
- const allowList = this.normalizeStringList(this.openclawStructured.toolsAllow);
5911
- const denyList = this.normalizeStringList(this.openclawStructured.toolsDeny);
5912
- const hasTools = profile || allowList.length || denyList.length || (config.tools && typeof config.tools === 'object');
5913
- if (hasTools) {
5914
- const tools = config.tools && typeof config.tools === 'object' && !Array.isArray(config.tools)
5915
- ? config.tools
5916
- : {};
5917
- tools.profile = profile || tools.profile || 'default';
5918
- tools.allow = allowList;
5919
- tools.deny = denyList;
5920
- config.tools = tools;
5921
- }
5922
-
5923
- this.openclawEditing.content = this.stringifyOpenclawConfig(config);
5924
- this.refreshOpenclawProviders(config);
5925
- this.refreshOpenclawAgentsList(config);
5926
- this.fillOpenclawQuickFromConfig(config);
5927
- this.showMessage('已写入编辑器', 'success');
5928
- },
5929
-
5930
- applyOpenclawQuickToText() {
5931
- const parsed = this.parseOpenclawContent(this.openclawEditing.content, { allowEmpty: true });
5932
- if (!parsed.ok) {
5933
- this.showMessage('解析 OpenClaw 配置失败: ' + parsed.error, 'error');
5934
- return;
5935
- }
5936
-
5937
- const providerName = (this.openclawQuick.providerName || '').trim();
5938
- const modelId = (this.openclawQuick.modelId || '').trim();
5939
- if (!providerName) {
5940
- this.showMessage('请填写 Provider 名称', 'error');
5941
- return;
5942
- }
5943
- if (providerName.includes('/')) {
5944
- this.showMessage('Provider 名称不能包含 "/"', 'error');
5945
- return;
5946
- }
5947
- if (!modelId) {
5948
- this.showMessage('请填写模型 ID', 'error');
5949
- return;
5950
- }
5951
-
5952
- const config = parsed.data;
5953
- const ensureObject = (value) => (value && typeof value === 'object' && !Array.isArray(value)) ? value : {};
5954
- const models = ensureObject(config.models);
5955
- const providers = ensureObject(models.providers);
5956
- const provider = ensureObject(providers[providerName]);
5957
- const baseUrl = (this.openclawQuick.baseUrl || '').trim();
5958
- if (!baseUrl && !provider.baseUrl) {
5959
- this.showMessage('请填写 Base URL', 'error');
5960
- return;
5961
- }
5962
-
5963
- const contextWindow = this.parseOptionalNumber(this.openclawQuick.contextWindow, '上下文长度');
5964
- if (!contextWindow.ok) {
5965
- this.showMessage(contextWindow.error, 'error');
5966
- return;
5967
- }
5968
- const maxTokens = this.parseOptionalNumber(this.openclawQuick.maxTokens, '最大输出');
5969
- if (!maxTokens.ok) {
5970
- this.showMessage(maxTokens.error, 'error');
5971
- return;
5972
- }
5973
-
5974
- const shouldOverrideProvider = !!this.openclawQuick.overrideProvider;
5975
- const apiKey = (this.openclawQuick.apiKey || '').trim();
5976
- const apiType = (this.openclawQuick.apiType || '').trim();
5977
- const setProviderField = (key, value) => {
5978
- if (!value) return;
5979
- if (shouldOverrideProvider || provider[key] === undefined || provider[key] === null || provider[key] === '') {
5980
- provider[key] = value;
5981
- }
5982
- };
5983
- setProviderField('baseUrl', baseUrl);
5984
- setProviderField('api', apiType);
5985
- if (apiKey) {
5986
- setProviderField('apiKey', apiKey);
5987
- }
5988
-
5989
- const modelName = (this.openclawQuick.modelName || '').trim() || modelId;
5990
- const modelEntry = {
5991
- id: modelId,
5992
- name: modelName,
5993
- reasoning: false,
5994
- input: ['text'],
5995
- cost: {
5996
- input: 0,
5997
- output: 0,
5998
- cacheRead: 0,
5999
- cacheWrite: 0
6000
- }
6001
- };
6002
- if (contextWindow.value !== null) {
6003
- modelEntry.contextWindow = contextWindow.value;
6004
- }
6005
- if (maxTokens.value !== null) {
6006
- modelEntry.maxTokens = maxTokens.value;
6007
- }
6008
-
6009
- const existingModels = Array.isArray(provider.models) ? [...provider.models] : [];
6010
- if (this.openclawQuick.overrideModels || existingModels.length === 0) {
6011
- provider.models = [modelEntry];
6012
- } else {
6013
- const idx = existingModels.findIndex(item => item && item.id === modelId);
6014
- if (idx >= 0) {
6015
- existingModels[idx] = this.mergeOpenclawModelEntry(existingModels[idx], modelEntry, false);
6016
- } else {
6017
- existingModels.push(modelEntry);
6018
- }
6019
- provider.models = existingModels;
6020
- }
6021
-
6022
- providers[providerName] = provider;
6023
- models.providers = providers;
6024
- config.models = models;
6025
-
6026
- if (this.openclawQuick.setPrimary) {
6027
- const agents = ensureObject(config.agents);
6028
- const defaults = ensureObject(agents.defaults);
6029
- const modelConfig = defaults.model && typeof defaults.model === 'object' && !Array.isArray(defaults.model)
6030
- ? defaults.model
6031
- : {};
6032
- modelConfig.primary = `${providerName}/${modelId}`;
6033
- defaults.model = modelConfig;
6034
- agents.defaults = defaults;
6035
- config.agents = agents;
6036
- if (config.agent && typeof config.agent === 'object' && !Array.isArray(config.agent)) {
6037
- config.agent.model = modelConfig.primary;
6038
- }
6039
- }
6040
-
6041
- this.openclawEditing.content = this.stringifyOpenclawConfig(config);
6042
- this.fillOpenclawStructured(config);
6043
- this.refreshOpenclawProviders(config);
6044
- this.refreshOpenclawAgentsList(config);
6045
- this.showMessage('快速配置已写入编辑器', 'success');
6046
- },
6047
-
6048
- addOpenclawFallback() {
6049
- this.openclawStructured.agentFallbacks.push('');
6050
- },
6051
-
6052
- removeOpenclawFallback(index) {
6053
- this.openclawStructured.agentFallbacks.splice(index, 1);
6054
- if (this.openclawStructured.agentFallbacks.length === 0) {
6055
- this.openclawStructured.agentFallbacks.push('');
6056
- }
6057
- },
6058
-
6059
- addOpenclawEnvItem() {
6060
- this.openclawStructured.envItems.push({ key: '', value: '', show: false });
6061
- },
6062
-
6063
- removeOpenclawEnvItem(index) {
6064
- this.openclawStructured.envItems.splice(index, 1);
6065
- if (this.openclawStructured.envItems.length === 0) {
6066
- this.openclawStructured.envItems.push({ key: '', value: '', show: false });
6067
- }
6068
- },
6069
-
6070
- toggleOpenclawEnvItem(index) {
6071
- const item = this.openclawStructured.envItems[index];
6072
- if (item) {
6073
- item.show = !item.show;
6074
- }
6075
- },
6076
-
6077
- addOpenclawToolsAllow() {
6078
- this.openclawStructured.toolsAllow.push('');
6079
- },
6080
-
6081
- removeOpenclawToolsAllow(index) {
6082
- this.openclawStructured.toolsAllow.splice(index, 1);
6083
- if (this.openclawStructured.toolsAllow.length === 0) {
6084
- this.openclawStructured.toolsAllow.push('');
6085
- }
6086
- },
6087
-
6088
- addOpenclawToolsDeny() {
6089
- this.openclawStructured.toolsDeny.push('');
6090
- },
6091
-
6092
- removeOpenclawToolsDeny(index) {
6093
- this.openclawStructured.toolsDeny.splice(index, 1);
6094
- if (this.openclawStructured.toolsDeny.length === 0) {
6095
- this.openclawStructured.toolsDeny.push('');
6096
- }
6097
- },
6098
-
6099
- openclawHasContent(config) {
6100
- return !!(config && typeof config.content === 'string' && config.content.trim());
6101
- },
6102
-
6103
- openclawSubtitle(config) {
6104
- if (!this.openclawHasContent(config)) {
6105
- return '未设置配置';
6106
- }
6107
- const length = config.content.trim().length;
6108
- return `已保存 ${length} 字符`;
6109
- },
6110
-
6111
- saveOpenclawConfigs() {
6112
- localStorage.setItem('openclawConfigs', JSON.stringify(this.openclawConfigs));
6113
- },
6114
-
6115
- openOpenclawAddModal() {
6116
- this.openclawEditorTitle = '添加 OpenClaw 配置';
6117
- this.openclawEditing = {
6118
- name: '',
6119
- content: '',
6120
- lockName: false
6121
- };
6122
- this.openclawConfigPath = '';
6123
- this.openclawConfigExists = false;
6124
- this.openclawLineEnding = '\n';
6125
- void this.loadOpenclawConfigFromFile({ silent: true, force: true, fallbackToTemplate: true });
6126
- this.showOpenclawConfigModal = true;
6127
- },
6128
-
6129
- openOpenclawEditModal(name) {
6130
- this.openclawEditorTitle = `编辑 OpenClaw 配置: ${name}`;
6131
- this.openclawEditing = {
6132
- name,
6133
- content: '',
6134
- lockName: true
6135
- };
6136
- void this.loadOpenclawConfigFromFile({ silent: true, force: true, fallbackToTemplate: true });
6137
- this.showOpenclawConfigModal = true;
6138
- },
6139
-
6140
- closeOpenclawConfigModal() {
6141
- this.showOpenclawConfigModal = false;
6142
- this.openclawEditing = { name: '', content: '', lockName: false };
6143
- this.openclawSaving = false;
6144
- this.openclawApplying = false;
6145
- this.resetOpenclawStructured();
6146
- this.resetOpenclawQuick();
6147
- },
6148
-
6149
- async loadOpenclawConfigFromFile(options = {}) {
6150
- const silent = !!options.silent;
6151
- const force = !!options.force;
6152
- const fallbackToTemplate = options.fallbackToTemplate !== false;
6153
- this.openclawFileLoading = true;
6154
- try {
6155
- const res = await api('get-openclaw-config');
6156
- if (res.error) {
6157
- if (!silent) {
6158
- this.showMessage(res.error, 'error');
6159
- }
6160
- return;
6161
- }
6162
- this.openclawConfigPath = res.path || '';
6163
- this.openclawConfigExists = !!res.exists;
6164
- this.openclawLineEnding = res.lineEnding === '\r\n' ? '\r\n' : '\n';
6165
- const hasContent = !!(res.content && res.content.trim());
6166
- const shouldOverride = force || !this.openclawEditing.content || !this.openclawEditing.content.trim();
6167
- if (hasContent && shouldOverride) {
6168
- this.openclawEditing.content = res.content;
6169
- } else if (!hasContent && shouldOverride && fallbackToTemplate) {
6170
- this.openclawEditing.content = DEFAULT_OPENCLAW_TEMPLATE;
6171
- }
6172
- this.syncOpenclawStructuredFromText({ silent: true });
6173
- if (!silent) {
6174
- this.showMessage('已加载当前 OpenClaw 配置', 'success');
6175
- }
6176
- } catch (e) {
6177
- if (!silent) {
6178
- this.showMessage('加载 OpenClaw 配置失败: ' + e.message, 'error');
6179
- }
6180
- } finally {
6181
- this.openclawFileLoading = false;
6182
- }
6183
- },
6184
-
6185
- persistOpenclawConfig({ closeModal = true } = {}) {
6186
- if (!this.openclawEditing.name || !this.openclawEditing.name.trim()) {
6187
- this.showMessage('请输入配置名称', 'error');
6188
- return '';
6189
- }
6190
- const name = this.openclawEditing.name.trim();
6191
- if (!this.openclawEditing.lockName && this.openclawConfigs[name]) {
6192
- this.showMessage('配置名称已存在', 'error');
6193
- return '';
6194
- }
6195
- if (!this.openclawEditing.content || !this.openclawEditing.content.trim()) {
6196
- this.showMessage('配置内容不能为空', 'error');
6197
- return '';
6198
- }
6199
-
6200
- this.openclawConfigs[name] = {
6201
- content: this.openclawEditing.content
6202
- };
6203
- this.currentOpenclawConfig = name;
6204
- this.saveOpenclawConfigs();
6205
- if (closeModal) {
6206
- this.closeOpenclawConfigModal();
6207
- }
6208
- return name;
6209
- },
6210
-
6211
- async saveOpenclawConfig() {
6212
- this.openclawSaving = true;
6213
- try {
6214
- const name = this.persistOpenclawConfig();
6215
- if (!name) return;
6216
- this.showMessage('OpenClaw 配置已保存', 'success');
6217
- } finally {
6218
- this.openclawSaving = false;
6219
- }
6220
- },
6221
-
6222
- async saveAndApplyOpenclawConfig() {
6223
- this.openclawApplying = true;
6224
- try {
6225
- const name = this.persistOpenclawConfig({ closeModal: false });
6226
- if (!name) return;
6227
- const config = this.openclawConfigs[name];
6228
- const res = await api('apply-openclaw-config', {
6229
- content: config.content,
6230
- lineEnding: this.openclawLineEnding
6231
- });
6232
- if (res.error || res.success === false) {
6233
- this.showMessage(res.error || '应用 OpenClaw 配置失败', 'error');
6234
- return;
6235
- }
6236
- this.openclawConfigPath = res.targetPath || this.openclawConfigPath;
6237
- this.openclawConfigExists = true;
6238
- const targetTip = res.targetPath ? `(${res.targetPath})` : '';
6239
- this.showMessage(`已保存并应用 OpenClaw 配置${targetTip}`, 'success');
6240
- this.closeOpenclawConfigModal();
6241
- } catch (e) {
6242
- this.showMessage('应用 OpenClaw 配置失败: ' + e.message, 'error');
6243
- } finally {
6244
- this.openclawApplying = false;
6245
- }
6246
- },
6247
-
6248
- deleteOpenclawConfig(name) {
6249
- if (Object.keys(this.openclawConfigs).length <= 1) {
6250
- return this.showMessage('至少保留一个配置', 'error');
6251
- }
6252
- if (!confirm(`确定删除配置 "${name}"?`)) return;
6253
- delete this.openclawConfigs[name];
6254
- if (this.currentOpenclawConfig === name) {
6255
- this.currentOpenclawConfig = Object.keys(this.openclawConfigs)[0];
6256
- }
6257
- this.saveOpenclawConfigs();
6258
- this.showMessage('OpenClaw 配置已删除', 'success');
6259
- },
6260
-
6261
- async applyOpenclawConfig(name) {
6262
- this.currentOpenclawConfig = name;
6263
- const config = this.openclawConfigs[name];
6264
- if (!this.openclawHasContent(config)) {
6265
- return this.showMessage('该配置为空,请先编辑', 'error');
6266
- }
6267
- const res = await api('apply-openclaw-config', {
6268
- content: config.content,
6269
- lineEnding: this.openclawLineEnding
6270
- });
6271
- if (res.error || res.success === false) {
6272
- this.showMessage(res.error || '应用 OpenClaw 配置失败', 'error');
6273
- } else {
6274
- this.openclawConfigPath = res.targetPath || this.openclawConfigPath;
6275
- this.openclawConfigExists = true;
6276
- const targetTip = res.targetPath ? `(${res.targetPath})` : '';
6277
- this.showMessage(`已应用 OpenClaw 配置: ${name}${targetTip}`, 'success');
6278
- }
6279
- },
6280
-
6281
- formatLatency(result) {
6282
- if (!result) return '';
6283
- if (!result.ok) return result.status ? `ERR ${result.status}` : 'ERR';
6284
- const ms = typeof result.durationMs === 'number' ? result.durationMs : 0;
6285
- return `${ms}ms`;
6286
- },
6287
-
6288
- buildSpeedTestIssue(name, result) {
6289
- if (!name || !result) return null;
6290
- if (result.error) {
6291
- const error = String(result.error || '');
6292
- const errorLower = error.toLowerCase();
6293
- if (error === 'Provider not found') {
6294
- return {
6295
- code: 'remote-speedtest-provider-missing',
6296
- message: `提供商 ${name} 未找到,无法测速`,
6297
- suggestion: '检查配置是否存在该 provider'
6298
- };
6299
- }
6300
- if (error === 'Provider missing URL' || error === 'Missing name or url') {
6301
- return {
6302
- code: 'remote-speedtest-baseurl-missing',
6303
- message: `提供商 ${name} 缺少 base_url`,
6304
- suggestion: '补全 base_url 后重试'
6305
- };
6306
- }
6307
- if (errorLower.includes('invalid url')) {
6308
- return {
6309
- code: 'remote-speedtest-invalid-url',
6310
- message: `提供商 ${name} 的 base_url 无效`,
6311
- suggestion: '请设置为 http/https 的完整 URL'
6312
- };
6313
- }
6314
- if (errorLower.includes('timeout')) {
6315
- return {
6316
- code: 'remote-speedtest-timeout',
6317
- message: `提供商 ${name} 远程测速超时`,
6318
- suggestion: '检查网络或 base_url 是否可达'
6319
- };
6320
- }
6321
- return {
6322
- code: 'remote-speedtest-unreachable',
6323
- message: `提供商 ${name} 远程测速失败:${error || '无法连接'}`,
6324
- suggestion: '检查网络或 base_url 是否可用'
6325
- };
6326
- }
6327
-
6328
- const status = typeof result.status === 'number' ? result.status : 0;
6329
- if (status === 401 || status === 403) {
6330
- return {
6331
- code: 'remote-speedtest-auth-failed',
6332
- message: `提供商 ${name} 远程测速鉴权失败(401/403)`,
6333
- suggestion: '检查 API Key 或认证方式'
6334
- };
6335
- }
6336
- if (status >= 400) {
6337
- return {
6338
- code: 'remote-speedtest-http-error',
6339
- message: `提供商 ${name} 远程测速返回异常状态: ${status}`,
6340
- suggestion: '检查 base_url 或服务状态'
6341
- };
6342
- }
6343
- return null;
6344
- },
6345
-
6346
- async runSpeedTest(name, options = {}) {
6347
- if (!name || this.speedLoading[name]) return null;
6348
- const silent = !!options.silent;
6349
- this.speedLoading[name] = true;
6350
- try {
6351
- const res = await api('speed-test', { name });
6352
- if (res.error) {
6353
- this.speedResults[name] = { ok: false, error: res.error };
6354
- if (!silent) {
6355
- this.showMessage(res.error, 'error');
6356
- }
6357
- return { ok: false, error: res.error };
6358
- }
6359
- this.speedResults[name] = res;
6360
- if (!silent) {
6361
- const status = res.status ? ` (${res.status})` : '';
6362
- this.showMessage(`Speed ${name}: ${this.formatLatency(res)}${status}`, 'success');
6363
- }
6364
- return res;
6365
- } catch (e) {
6366
- const message = e && e.message ? e.message : 'Speed test failed';
6367
- this.speedResults[name] = { ok: false, error: message };
6368
- if (!silent) {
6369
- this.showMessage(message, 'error');
6370
- }
6371
- return { ok: false, error: message };
6372
- } finally {
6373
- this.speedLoading[name] = false;
6374
- }
6375
- },
6376
-
6377
- async runClaudeSpeedTest(name, config) {
6378
- if (!name || this.claudeSpeedLoading[name]) return null;
6379
- const baseUrl = config && typeof config.baseUrl === 'string' ? config.baseUrl.trim() : '';
6380
- this.claudeSpeedLoading[name] = true;
6381
- try {
6382
- if (!baseUrl) {
6383
- const res = { ok: false, error: 'Missing base URL' };
6384
- this.claudeSpeedResults[name] = res;
6385
- return res;
6386
- }
6387
- const res = await api('speed-test', { url: baseUrl });
6388
- if (res.error) {
6389
- this.claudeSpeedResults[name] = { ok: false, error: res.error };
6390
- return { ok: false, error: res.error };
6391
- }
6392
- this.claudeSpeedResults[name] = res;
6393
- return res;
6394
- } catch (e) {
6395
- const message = e && e.message ? e.message : 'Speed test failed';
6396
- const res = { ok: false, error: message };
6397
- this.claudeSpeedResults[name] = res;
6398
- return res;
6399
- } finally {
6400
- this.claudeSpeedLoading[name] = false;
6401
- }
6402
- },
6403
-
6404
- showMessage(text, type) {
6405
- this.message = text;
6406
- this.messageType = type || 'info';
6407
- setTimeout(() => {
6408
- this.message = '';
6409
- }, 3000);
6410
- }
6411
- }
6412
- });
6413
-
6414
- app.mount('#app');
6415
- </script>
3975
+ <script type="module" src="web-ui/app.js"></script>
6416
3976
  </body>
6417
3977
  </html>