codexmate 0.0.8 → 0.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.planning/.fix-attempts +1 -0
- package/.planning/.lock +6 -0
- package/.planning/.verify-cache.json +14 -0
- package/.planning/CHECKPOINT.json +46 -0
- package/.planning/DESIGN.md +26 -0
- package/.planning/HISTORY.json +124 -0
- package/.planning/PLAN.md +69 -0
- package/.planning/REVIEW.md +41 -0
- package/.planning/STATE.md +12 -0
- package/.planning/STATS.json +13 -0
- package/.planning/VERIFICATION.md +70 -0
- package/.planning/daude-code-plan.md +51 -0
- package/.planning/research/architecture.md +32 -0
- package/.planning/research/conventions.md +36 -0
- package/.planning/task_1-REVIEW.md +29 -0
- package/.planning/task_1-SUMMARY.md +32 -0
- package/.planning/task_2-REVIEW.md +24 -0
- package/.planning/task_2-SUMMARY.md +37 -0
- package/.planning/task_3-REVIEW.md +25 -0
- package/.planning/task_3-SUMMARY.md +31 -0
- package/README.md +11 -12
- package/README.zh-CN.md +25 -20
- package/cli.js +291 -156
- package/lib/cli-file-utils.js +9 -7
- package/package.json +3 -2
- package/res/json5.min.js +1 -0
- package/res/vue.global.js +18552 -0
- package/tests/e2e/run.js +19 -4
- package/tests/e2e/test-health-speed.js +5 -1
- package/tests/e2e/test-session-search.js +114 -0
- package/tests/e2e/test-sessions.js +22 -13
- package/tests/e2e/test-setup.js +83 -14
- package/tests/unit/run.mjs +29 -0
- package/tests/unit/web-ui-logic.test.mjs +186 -0
- package/web-ui/app.js +2841 -0
- package/web-ui/logic.mjs +157 -0
- package/web-ui.html +574 -3014
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="
|
|
8
|
-
<script src="
|
|
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:
|
|
171
|
+
max-width: 1280px;
|
|
172
172
|
margin: 0 auto;
|
|
173
|
-
padding:
|
|
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
|
-
.
|
|
189
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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.
|
|
519
|
-
border-color: rgba(201, 94, 75, 0.
|
|
520
|
-
box-shadow:
|
|
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:
|
|
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:
|
|
618
|
-
height:
|
|
619
|
-
border-radius:
|
|
620
|
-
border:
|
|
621
|
-
background:
|
|
622
|
-
color: var(--color-text-
|
|
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,
|
|
632
|
-
color: var(--color-text-
|
|
633
|
-
transform:
|
|
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:
|
|
643
|
-
height:
|
|
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:
|
|
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(
|
|
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:
|
|
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
|
|
1332
|
-
|
|
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:
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
border: 1px solid
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
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
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
-
|
|
2601
|
-
|
|
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
|
-
|
|
2692
|
-
|
|
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
|
-
|
|
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="
|
|
2792
|
-
<option value="
|
|
2793
|
-
|
|
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>
|