codexmate 0.0.7 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/.github/workflows/release.yml +122 -8
  2. package/.planning/.fix-attempts +1 -0
  3. package/.planning/.lock +6 -0
  4. package/.planning/.verify-cache.json +14 -0
  5. package/.planning/CHECKPOINT.json +46 -0
  6. package/.planning/DESIGN.md +26 -0
  7. package/.planning/HISTORY.json +124 -0
  8. package/.planning/PLAN.md +69 -0
  9. package/.planning/REVIEW.md +41 -0
  10. package/.planning/STATE.md +12 -0
  11. package/.planning/STATS.json +13 -0
  12. package/.planning/VERIFICATION.md +70 -0
  13. package/.planning/daude-code-plan.md +51 -0
  14. package/.planning/research/architecture.md +32 -0
  15. package/.planning/research/conventions.md +36 -0
  16. package/.planning/task_1-REVIEW.md +29 -0
  17. package/.planning/task_1-SUMMARY.md +32 -0
  18. package/.planning/task_2-REVIEW.md +24 -0
  19. package/.planning/task_2-SUMMARY.md +37 -0
  20. package/.planning/task_3-REVIEW.md +25 -0
  21. package/.planning/task_3-SUMMARY.md +31 -0
  22. package/README.md +58 -52
  23. package/README.zh-CN.md +68 -56
  24. package/cli.js +1142 -1427
  25. package/lib/cli-file-utils.js +151 -0
  26. package/lib/cli-models-utils.js +152 -0
  27. package/lib/cli-network-utils.js +148 -0
  28. package/lib/cli-session-utils.js +121 -0
  29. package/lib/cli-utils.js +139 -0
  30. package/package.json +4 -2
  31. package/res/json5.min.js +1 -0
  32. package/res/vue.global.js +18552 -0
  33. package/tests/e2e/helpers.js +214 -0
  34. package/tests/e2e/recent-health.e2e.js +6 -0
  35. package/tests/e2e/run.js +103 -306
  36. package/tests/e2e/test-claude.js +21 -0
  37. package/tests/e2e/test-config.js +124 -0
  38. package/tests/e2e/test-health-speed.js +79 -0
  39. package/tests/e2e/test-openclaw.js +47 -0
  40. package/tests/e2e/test-session-search.js +114 -0
  41. package/tests/e2e/test-sessions.js +69 -0
  42. package/tests/e2e/test-setup.js +159 -0
  43. package/tests/unit/run.mjs +29 -0
  44. package/tests/unit/web-ui-logic.test.mjs +186 -0
  45. package/web-ui/app.js +2841 -0
  46. package/web-ui/logic.mjs +157 -0
  47. package/web-ui.html +1045 -2996
package/web-ui.html CHANGED
@@ -4,8 +4,8 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Codex Mate</title>
7
- <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
8
- <script src="https://unpkg.com/json5@2/dist/index.min.js"></script>
7
+ <script src="res/vue.global.js"></script>
8
+ <script src="res/json5.min.js"></script>
9
9
  <style>
10
10
  @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&family=Source+Sans+3:wght@400;500;600&family=Space+Grotesk:wght@400;500;600;700&display=swap');
11
11
 
@@ -13,10 +13,10 @@
13
13
  设计系统 - Design Tokens
14
14
  ============================================ */
15
15
  :root {
16
- /* 色彩系统 */
17
- --color-brand: #C95E4B;
18
- --color-brand-dark: #A94637;
19
- --color-brand-light: rgba(201, 94, 75, 0.14);
16
+ /* 色彩系统:去除杂纹,强调干净留白与温柔橙红 */
17
+ --color-brand: #D0583A;
18
+ --color-brand-dark: #B8442B;
19
+ --color-brand-light: rgba(208, 88, 58, 0.14);
20
20
  --color-brand-subtle: rgba(201, 94, 75, 0.2);
21
21
 
22
22
  --color-bg: #F6EFE6;
@@ -32,8 +32,8 @@
32
32
  --color-border-soft: rgba(216, 201, 184, 0.45);
33
33
  --color-border-strong: rgba(216, 201, 184, 0.8);
34
34
 
35
- --color-success: #4E8B66;
36
- --color-error: #C1483B;
35
+ --color-success: #4B8B6A;
36
+ --color-error: #C44536;
37
37
 
38
38
  --bg-warm-gradient:
39
39
  radial-gradient(circle at 16% 10%, rgba(201, 94, 75, 0.18), transparent 45%),
@@ -133,6 +133,9 @@
133
133
  overflow-x: hidden;
134
134
  }
135
135
 
136
+ /* ============================================
137
+ 容器
138
+ ============================================ */
136
139
  body::before {
137
140
  content: "";
138
141
  position: fixed;
@@ -146,18 +149,355 @@
146
149
  z-index: 0;
147
150
  }
148
151
 
152
+ /* 背景网格 */
153
+ body::after {
154
+ content: "";
155
+ position: fixed;
156
+ inset: 0;
157
+ background-image:
158
+ linear-gradient(90deg, rgba(255, 255, 255, 0.15) 1px, transparent 1px),
159
+ linear-gradient(0deg, rgba(255, 255, 255, 0.12) 1px, transparent 1px);
160
+ background-size: 120px 120px;
161
+ opacity: 0.5;
162
+ pointer-events: none;
163
+ z-index: 0;
164
+ }
165
+
149
166
  /* ============================================
150
167
  容器
151
168
  ============================================ */
152
169
  .container {
153
170
  width: 100%;
154
- max-width: 1160px;
171
+ max-width: 1280px;
155
172
  margin: 0 auto;
156
- padding-bottom: var(--spacing-lg);
173
+ padding: 16px 12px 28px;
157
174
  position: relative;
158
175
  z-index: 1;
159
176
  }
160
177
 
178
+ /* ============================================
179
+ 布局:双列(侧栏 + 主区)
180
+ ============================================ */
181
+ .app-shell {
182
+ display: grid;
183
+ grid-template-columns: 320px minmax(0, 1fr);
184
+ gap: var(--spacing-md);
185
+ align-items: flex-start;
186
+ }
187
+
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;
278
+ }
279
+
280
+ .brand-block {
281
+ margin-bottom: var(--spacing-md);
282
+ }
283
+
284
+ .brand-title {
285
+ font-size: 30px;
286
+ line-height: 1.05;
287
+ font-family: var(--font-family-display);
288
+ color: var(--color-text-primary);
289
+ letter-spacing: -0.02em;
290
+ }
291
+
292
+ .brand-title .accent {
293
+ color: var(--color-brand);
294
+ }
295
+
296
+ .brand-subtitle {
297
+ margin-top: 8px;
298
+ font-size: var(--font-size-secondary);
299
+ color: var(--color-text-tertiary);
300
+ line-height: 1.45;
301
+ }
302
+
303
+ .main-tabs {
304
+ display: flex;
305
+ gap: 10px;
306
+ }
307
+
308
+ .main-tab-btn {
309
+ flex: 1;
310
+ text-align: center;
311
+ border: 1px solid rgba(216, 201, 184, 0.55);
312
+ background: rgba(255, 255, 255, 0.95);
313
+ border-radius: var(--radius-lg);
314
+ padding: 12px 14px;
315
+ cursor: pointer;
316
+ color: var(--color-text-secondary);
317
+ font-size: var(--font-size-body);
318
+ font-weight: var(--font-weight-secondary);
319
+ box-shadow: var(--shadow-subtle);
320
+ transition: all var(--transition-normal) var(--ease-spring);
321
+ }
322
+
323
+ .main-tab-btn:hover {
324
+ border-color: var(--color-brand);
325
+ color: var(--color-text-primary);
326
+ transform: translateY(-1px);
327
+ }
328
+
329
+ .main-tab-btn.active {
330
+ border-color: var(--color-brand);
331
+ box-shadow: 0 10px 24px rgba(27, 23, 20, 0.08);
332
+ color: var(--color-text-primary);
333
+ background: linear-gradient(135deg, rgba(201, 94, 75, 0.12), rgba(255, 255, 255, 0.95));
334
+ }
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
+
370
+ .main-panel {
371
+ min-width: 0;
372
+ background: rgba(255, 255, 255, 0.9);
373
+ border: 1px solid rgba(255, 255, 255, 0.65);
374
+ border-radius: 18px;
375
+ box-shadow: 0 12px 30px rgba(27, 23, 20, 0.08);
376
+ padding: var(--spacing-md) var(--spacing-lg);
377
+ backdrop-filter: blur(8px);
378
+ position: relative;
379
+ overflow-x: hidden;
380
+ overflow-y: visible;
381
+ }
382
+
383
+ .panel-header {
384
+ margin-bottom: 12px;
385
+ text-align: left;
386
+ }
387
+
388
+ .hero {
389
+ margin-bottom: var(--spacing-sm);
390
+ }
391
+
392
+ .hero-title {
393
+ font-size: 48px;
394
+ line-height: 1.05;
395
+ font-family: var(--font-family-display);
396
+ color: var(--color-text-primary);
397
+ letter-spacing: -0.02em;
398
+ }
399
+
400
+ .hero-title .accent {
401
+ color: var(--color-brand);
402
+ }
403
+
404
+ .hero-subtitle {
405
+ margin-top: 8px;
406
+ font-size: var(--font-size-body);
407
+ color: var(--color-text-tertiary);
408
+ line-height: 1.5;
409
+ }
410
+
411
+ .top-tabs {
412
+ margin: 14px 0 18px;
413
+ background: rgba(255, 255, 255, 0.92);
414
+ border: 1px solid rgba(255, 255, 255, 0.7);
415
+ border-radius: 14px;
416
+ padding: 6px;
417
+ box-shadow: inset 0 1px 2px rgba(31, 26, 23, 0.06);
418
+ display: grid;
419
+ grid-template-columns: repeat(4, 1fr);
420
+ gap: 8px;
421
+ backdrop-filter: blur(6px);
422
+ }
423
+
424
+ .top-tab {
425
+ border: 1px solid rgba(216, 201, 184, 0.55);
426
+ border-radius: 12px;
427
+ background: rgba(255, 255, 255, 0.96);
428
+ padding: 11px 10px;
429
+ font-size: var(--font-size-body);
430
+ color: var(--color-text-secondary);
431
+ text-align: center;
432
+ cursor: pointer;
433
+ transition: all var(--transition-normal) var(--ease-spring);
434
+ box-shadow: var(--shadow-subtle);
435
+ }
436
+
437
+ .top-tab:hover {
438
+ border-color: var(--color-brand);
439
+ color: var(--color-text-primary);
440
+ transform: translateY(-1px);
441
+ }
442
+
443
+ .top-tab.active {
444
+ border-color: var(--color-brand);
445
+ color: var(--color-text-primary);
446
+ background: linear-gradient(135deg, rgba(201, 94, 75, 0.12), rgba(255, 255, 255, 0.95));
447
+ box-shadow: 0 10px 24px rgba(27, 23, 20, 0.08);
448
+ }
449
+
450
+ .config-subtabs {
451
+ display: flex;
452
+ gap: 8px;
453
+ margin-bottom: 16px;
454
+ padding: 6px;
455
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.85), rgba(255, 255, 255, 0.7));
456
+ border-radius: var(--radius-lg);
457
+ border: 1px solid rgba(255, 255, 255, 0.7);
458
+ box-shadow: inset 0 1px 2px rgba(31, 26, 23, 0.05);
459
+ }
460
+
461
+ .config-subtab {
462
+ border: 1px solid var(--color-border-soft);
463
+ border-radius: var(--radius-lg);
464
+ padding: 10px 14px;
465
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.92), rgba(255, 250, 245, 0.9));
466
+ color: var(--color-text-secondary);
467
+ cursor: pointer;
468
+ font-size: var(--font-size-body);
469
+ font-weight: var(--font-weight-secondary);
470
+ transition: all var(--transition-normal) var(--ease-spring);
471
+ box-shadow: var(--shadow-subtle);
472
+ }
473
+
474
+ .config-subtab:hover {
475
+ border-color: var(--color-border-strong);
476
+ color: var(--color-text-primary);
477
+ }
478
+
479
+ .config-subtab.active {
480
+ border-color: var(--color-brand);
481
+ color: var(--color-text-primary);
482
+ background: linear-gradient(135deg, rgba(201, 94, 75, 0.18), rgba(255, 255, 255, 0.95));
483
+ box-shadow: var(--shadow-card);
484
+ }
485
+
486
+ .content-wrapper {
487
+ background: rgba(255, 255, 255, 0.9);
488
+ border: 1px solid rgba(255, 255, 255, 0.7);
489
+ border-radius: var(--radius-lg);
490
+ box-shadow: var(--shadow-card);
491
+ padding: 0;
492
+ }
493
+
494
+ .mode-content {
495
+ border-radius: var(--radius-lg);
496
+ background: rgba(255, 255, 255, 0.85);
497
+ box-shadow: var(--shadow-subtle);
498
+ padding: 10px;
499
+ }
500
+
161
501
  /* ============================================
162
502
  主标题
163
503
  ============================================ */
@@ -195,14 +535,14 @@
195
535
  ============================================ */
196
536
  .segmented-control {
197
537
  display: flex;
198
- background: linear-gradient(180deg, rgba(255, 255, 255, 0.85) 0%, rgba(255, 255, 255, 0.55) 100%);
199
- border-radius: var(--radius-lg);
200
- padding: 5px;
201
- margin-bottom: 16px;
538
+ background: rgba(255, 255, 255, 0.92);
539
+ border-radius: var(--radius-xl);
540
+ padding: 6px;
541
+ margin-bottom: 20px;
202
542
  position: relative;
203
- box-shadow: inset 0 1px 2px rgba(31, 26, 23, 0.08);
543
+ box-shadow: inset 0 1px 2px rgba(31, 26, 23, 0.06);
204
544
  border: 1px solid rgba(255, 255, 255, 0.7);
205
- backdrop-filter: blur(10px);
545
+ backdrop-filter: blur(6px);
206
546
  }
207
547
 
208
548
  .segment {
@@ -245,9 +585,9 @@
245
585
  卡片
246
586
  ============================================ */
247
587
  .card {
248
- background: linear-gradient(to bottom, var(--color-surface) 0%, rgba(255, 255, 255, 0.95) 100%);
588
+ background: linear-gradient(180deg, #fffdf9 0%, #fff8f2 100%);
249
589
  border-radius: var(--radius-lg);
250
- padding: var(--spacing-sm);
590
+ padding: 10px;
251
591
  display: flex;
252
592
  align-items: center;
253
593
  justify-content: space-between;
@@ -256,17 +596,17 @@
256
596
  transform var(--transition-normal) var(--ease-spring),
257
597
  box-shadow var(--transition-normal) var(--ease-spring),
258
598
  background-color var(--transition-fast) var(--ease-smooth);
259
- box-shadow: var(--shadow-card);
599
+ box-shadow: 0 10px 24px rgba(27, 23, 20, 0.08);
260
600
  user-select: none;
261
601
  will-change: transform;
262
- border: 1px solid var(--color-border-soft);
602
+ border: 1px solid rgba(216, 201, 184, 0.55);
263
603
  position: relative;
264
604
  overflow: hidden;
265
605
  }
266
606
 
267
607
  .card:hover {
268
608
  transform: translateY(-2px) scale(1.005);
269
- box-shadow: var(--shadow-float);
609
+ box-shadow: 0 16px 32px rgba(27, 23, 20, 0.1);
270
610
  }
271
611
 
272
612
  .card::before,
@@ -300,9 +640,9 @@
300
640
  }
301
641
 
302
642
  .card.active {
303
- background: linear-gradient(to bottom, rgba(210, 107, 90, 0.12) 0%, rgba(255, 255, 255, 0.98) 100%);
304
- border-color: rgba(201, 94, 75, 0.45);
305
- box-shadow: var(--shadow-float);
643
+ background: linear-gradient(to bottom, rgba(210, 107, 90, 0.14) 0%, rgba(255, 255, 255, 0.98) 100%);
644
+ border-color: rgba(201, 94, 75, 0.55);
645
+ box-shadow: 0 10px 28px rgba(210, 107, 90, 0.14);
306
646
  }
307
647
 
308
648
  .card.active::before {
@@ -382,7 +722,7 @@
382
722
  /* 卡片操作按钮 - hover 显示 */
383
723
  .card-actions {
384
724
  display: flex;
385
- gap: 4px;
725
+ gap: 8px;
386
726
  opacity: 0;
387
727
  transform: translateX(4px);
388
728
  transition: all var(--transition-normal) var(--ease-spring);
@@ -393,24 +733,30 @@
393
733
  transform: translateX(0);
394
734
  }
395
735
 
736
+ .mode-cards .card-actions {
737
+ opacity: 1;
738
+ transform: translateX(0);
739
+ }
740
+
396
741
  .card-action-btn {
397
- width: 28px;
398
- height: 28px;
399
- border-radius: var(--radius-sm);
400
- border: none;
401
- background: transparent;
402
- color: var(--color-text-tertiary);
742
+ width: 40px;
743
+ height: 40px;
744
+ border-radius: 10px;
745
+ border: 1px solid rgba(70, 86, 110, 0.22);
746
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(255, 255, 255, 0.9));
747
+ color: var(--color-text-secondary);
403
748
  cursor: pointer;
404
749
  display: flex;
405
750
  align-items: center;
406
751
  justify-content: center;
407
752
  transition: all var(--transition-fast) var(--ease-spring);
753
+ box-shadow: inset 0 1px 2px rgba(31, 26, 23, 0.04);
408
754
  }
409
755
 
410
756
  .card-action-btn:hover {
411
- background: linear-gradient(135deg, var(--color-bg) 0%, rgba(247, 241, 232, 0.8) 100%);
412
- color: var(--color-text-secondary);
413
- transform: scale(1.08);
757
+ background: linear-gradient(135deg, rgba(210, 107, 90, 0.08) 0%, rgba(255, 255, 255, 0.95) 100%);
758
+ color: var(--color-text-primary);
759
+ transform: translateY(-1px);
414
760
  }
415
761
 
416
762
  .card-action-btn.delete:hover {
@@ -419,8 +765,8 @@
419
765
  }
420
766
 
421
767
  .card-action-btn svg {
422
- width: 16px;
423
- height: 16px;
768
+ width: 18px;
769
+ height: 18px;
424
770
  }
425
771
 
426
772
  /* ============================================
@@ -524,54 +870,6 @@
524
870
  gap: var(--spacing-xs);
525
871
  }
526
872
 
527
- .recent-list {
528
- display: flex;
529
- flex-wrap: wrap;
530
- gap: 8px;
531
- margin-top: 8px;
532
- }
533
-
534
- .recent-item {
535
- border: 1px solid var(--color-border-soft);
536
- background: var(--color-surface-elevated);
537
- border-radius: var(--radius-md);
538
- padding: 10px 12px;
539
- min-width: 170px;
540
- text-align: left;
541
- cursor: pointer;
542
- transition: all var(--transition-fast) var(--ease-spring);
543
- box-shadow: 0 2px 6px rgba(27, 23, 20, 0.06);
544
- }
545
-
546
- .recent-item:hover {
547
- transform: translateY(-1px);
548
- box-shadow: 0 4px 12px rgba(27, 23, 20, 0.12);
549
- border-color: var(--color-border);
550
- }
551
-
552
- .recent-item:disabled {
553
- opacity: 0.6;
554
- cursor: not-allowed;
555
- }
556
-
557
- .recent-provider {
558
- font-size: var(--font-size-body);
559
- font-weight: var(--font-weight-secondary);
560
- color: var(--color-text-primary);
561
- margin-bottom: 4px;
562
- }
563
-
564
- .recent-model {
565
- font-size: var(--font-size-caption);
566
- color: var(--color-text-tertiary);
567
- line-height: 1.4;
568
- }
569
-
570
- .recent-empty {
571
- font-size: var(--font-size-caption);
572
- color: var(--color-text-tertiary);
573
- }
574
-
575
873
  .health-report {
576
874
  margin-top: 10px;
577
875
  padding: 10px 12px;
@@ -657,7 +955,7 @@
657
955
  outline: none;
658
956
  cursor: pointer;
659
957
  appearance: none;
660
- background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='none' stroke='%235C5046' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' d='M2 4l4 4 4-4'/%3E%3C/svg%3E");
958
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='none' stroke='%23505A66' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' d='M2 4l4 4 4-4'/%3E%3C/svg%3E");
661
959
  background-repeat: no-repeat;
662
960
  background-position: right 14px center;
663
961
  background-size: 12px;
@@ -807,6 +1105,32 @@
807
1105
  justify-content: flex-end;
808
1106
  }
809
1107
 
1108
+ .session-toolbar-footer {
1109
+ display: flex;
1110
+ align-items: center;
1111
+ justify-content: flex-end;
1112
+ gap: var(--spacing-xs);
1113
+ margin-top: -2px;
1114
+ padding-top: 6px;
1115
+ margin-bottom: 12px;
1116
+ border-top: 1px dashed var(--color-border-soft);
1117
+ }
1118
+
1119
+ .session-toolbar-footer .quick-option {
1120
+ margin: 0;
1121
+ padding: 6px 10px;
1122
+ border-radius: var(--radius-sm);
1123
+ border: 1px solid var(--color-border-soft);
1124
+ background: linear-gradient(to bottom, rgba(255, 255, 255, 0.95) 0%, rgba(255, 255, 255, 0.8) 100%);
1125
+ box-shadow: inset 0 1px 2px rgba(31, 26, 23, 0.04);
1126
+ transition: all var(--transition-fast) var(--ease-spring);
1127
+ line-height: 1.2;
1128
+ }
1129
+
1130
+ .session-toolbar-footer .quick-option:hover {
1131
+ border-color: var(--color-border-strong);
1132
+ }
1133
+
810
1134
  .session-source-select,
811
1135
  .session-path-select,
812
1136
  .session-query-input,
@@ -896,47 +1220,25 @@
896
1220
  }
897
1221
 
898
1222
  .session-count-pill {
1223
+ display: inline-flex;
1224
+ align-items: center;
1225
+ justify-content: center;
899
1226
  font-size: var(--font-size-caption);
900
1227
  color: var(--color-text-secondary);
901
1228
  border: 1px solid var(--color-border-soft);
902
1229
  border-radius: 999px;
903
- padding: 2px 8px;
1230
+ padding: 0 8px;
1231
+ height: 22px;
1232
+ line-height: 1;
904
1233
  background: rgba(247, 241, 232, 0.7);
905
1234
  white-space: nowrap;
906
1235
  margin-left: auto;
907
1236
  }
908
1237
 
909
- .btn-session-export {
910
- border: 1px solid var(--color-border-soft);
911
- border-radius: var(--radius-sm);
912
- background: linear-gradient(to bottom, var(--color-surface) 0%, rgba(255, 255, 255, 0.9) 100%);
913
- color: var(--color-text-secondary);
914
- padding: 8px 12px;
915
- font-size: var(--font-size-secondary);
916
- font-weight: var(--font-weight-secondary);
917
- cursor: pointer;
918
- transition: all var(--transition-fast) var(--ease-spring);
919
- white-space: nowrap;
920
- box-shadow: var(--shadow-subtle);
921
- letter-spacing: -0.01em;
922
- }
923
-
924
- .btn-session-open {
925
- border: 1px solid var(--color-border-soft);
926
- border-radius: var(--radius-sm);
927
- background: linear-gradient(to bottom, var(--color-surface) 0%, rgba(255, 255, 255, 0.9) 100%);
928
- color: var(--color-text-secondary);
929
- padding: 8px 12px;
930
- font-size: var(--font-size-secondary);
931
- font-weight: var(--font-weight-secondary);
932
- cursor: pointer;
933
- transition: all var(--transition-fast) var(--ease-spring);
934
- white-space: nowrap;
935
- box-shadow: var(--shadow-subtle);
936
- letter-spacing: -0.01em;
937
- }
938
-
939
- .btn-session-clone {
1238
+ .btn-session-export,
1239
+ .btn-session-open,
1240
+ .btn-session-clone,
1241
+ .btn-session-refresh {
940
1242
  border: 1px solid var(--color-border-soft);
941
1243
  border-radius: var(--radius-sm);
942
1244
  background: linear-gradient(to bottom, var(--color-surface) 0%, rgba(255, 255, 255, 0.9) 100%);
@@ -952,7 +1254,7 @@
952
1254
  }
953
1255
 
954
1256
  .btn-session-delete {
955
- border: 1px solid rgba(189, 70, 68, 0.4);
1257
+ border: 1px solid rgba(189, 70, 68, 0.45);
956
1258
  border-radius: var(--radius-sm);
957
1259
  background: linear-gradient(to bottom, rgba(255, 245, 245, 0.95) 0%, rgba(255, 255, 255, 0.9) 100%);
958
1260
  color: #b74545;
@@ -966,21 +1268,6 @@
966
1268
  letter-spacing: -0.01em;
967
1269
  }
968
1270
 
969
- .btn-session-refresh {
970
- border: 1px solid var(--color-border-soft);
971
- border-radius: var(--radius-sm);
972
- background: linear-gradient(to bottom, var(--color-surface) 0%, rgba(255, 255, 255, 0.9) 100%);
973
- color: var(--color-text-secondary);
974
- padding: 8px 12px;
975
- font-size: var(--font-size-secondary);
976
- font-weight: var(--font-weight-secondary);
977
- cursor: pointer;
978
- transition: all var(--transition-fast) var(--ease-spring);
979
- white-space: nowrap;
980
- box-shadow: var(--shadow-subtle);
981
- letter-spacing: -0.01em;
982
- }
983
-
984
1271
  .btn-session-refresh:hover {
985
1272
  border-color: var(--color-brand);
986
1273
  color: var(--color-brand);
@@ -1053,47 +1340,47 @@
1053
1340
  box-shadow: inset 0 0 0 6px rgba(255, 255, 255, 0.7);
1054
1341
  }
1055
1342
 
1056
- .session-layout {
1057
- display: grid;
1058
- grid-template-columns: minmax(280px, 360px) minmax(0, 1fr);
1059
- gap: var(--spacing-sm);
1060
- align-items: start;
1061
- height: min(72vh, 760px);
1062
- min-height: 520px;
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
+ }
1351
+
1352
+ .session-layout.session-standalone {
1353
+ grid-template-columns: minmax(0, 1fr);
1063
1354
  }
1064
1355
 
1065
- .session-layout.session-standalone {
1066
- grid-template-columns: minmax(0, 1fr);
1067
- }
1068
-
1069
- .session-standalone-page {
1070
- max-width: 960px;
1071
- margin: 0 auto;
1072
- padding: var(--spacing-sm) 0;
1073
- }
1074
-
1075
- .session-standalone-title {
1076
- font-size: var(--font-size-title);
1077
- font-weight: var(--font-weight-title);
1078
- color: var(--color-text-primary);
1079
- margin-bottom: var(--spacing-sm);
1080
- letter-spacing: -0.01em;
1081
- }
1082
-
1083
- .session-standalone-text {
1084
- white-space: pre-wrap;
1085
- font-family: var(--font-family-body);
1086
- font-size: var(--font-size-body);
1087
- line-height: 1.7;
1088
- color: var(--color-text-primary);
1089
- word-break: break-word;
1090
- }
1091
-
1092
- .session-list {
1093
- display: flex;
1094
- flex-direction: column;
1095
- gap: var(--spacing-xs);
1096
- position: sticky;
1356
+ .session-standalone-page {
1357
+ max-width: 960px;
1358
+ margin: 0 auto;
1359
+ padding: var(--spacing-sm) 0;
1360
+ }
1361
+
1362
+ .session-standalone-title {
1363
+ font-size: var(--font-size-title);
1364
+ font-weight: var(--font-weight-title);
1365
+ color: var(--color-text-primary);
1366
+ margin-bottom: var(--spacing-sm);
1367
+ letter-spacing: -0.01em;
1368
+ }
1369
+
1370
+ .session-standalone-text {
1371
+ white-space: pre-wrap;
1372
+ font-family: var(--font-family-body);
1373
+ font-size: var(--font-size-body);
1374
+ line-height: 1.7;
1375
+ color: var(--color-text-primary);
1376
+ word-break: break-word;
1377
+ }
1378
+
1379
+ .session-list {
1380
+ display: flex;
1381
+ flex-direction: column;
1382
+ gap: var(--spacing-xs);
1383
+ position: sticky;
1097
1384
  top: 12px;
1098
1385
  height: 100%;
1099
1386
  max-height: none;
@@ -1133,11 +1420,14 @@
1133
1420
  border: 1px solid var(--color-border-soft);
1134
1421
  border-radius: var(--radius-sm);
1135
1422
  background: linear-gradient(to bottom, var(--color-surface) 0%, rgba(255, 255, 255, 0.92) 100%);
1136
- padding: 10px 12px;
1423
+ padding: 14px 16px;
1137
1424
  cursor: pointer;
1138
1425
  transition: all var(--transition-fast) var(--ease-spring);
1139
1426
  user-select: none;
1140
1427
  min-width: 0;
1428
+ position: relative;
1429
+ overflow: hidden;
1430
+ min-height: 88px;
1141
1431
  }
1142
1432
 
1143
1433
  .session-item-header {
@@ -1162,6 +1452,18 @@
1162
1452
  transform: translateY(-1px);
1163
1453
  }
1164
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
+
1165
1467
  .session-item:active {
1166
1468
  transform: scale(0.99);
1167
1469
  }
@@ -1172,19 +1474,23 @@
1172
1474
  box-shadow: 0 6px 16px rgba(210, 107, 90, 0.12);
1173
1475
  }
1174
1476
 
1175
- .session-item-title {
1176
- font-size: var(--font-size-body);
1177
- font-weight: var(--font-weight-secondary);
1178
- color: var(--color-text-primary);
1179
- line-height: 1.35;
1180
- display: -webkit-box;
1181
- -webkit-line-clamp: 2;
1182
- -webkit-box-orient: vertical;
1183
- overflow: hidden;
1184
- word-break: break-word;
1185
- flex: 1;
1477
+ .session-item.active::before {
1478
+ background: linear-gradient(180deg, rgba(201, 94, 75, 0.9), rgba(201, 94, 75, 0.4));
1186
1479
  }
1187
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
+
1188
1494
  .session-item-actions {
1189
1495
  display: inline-flex;
1190
1496
  align-items: center;
@@ -1207,21 +1513,6 @@
1207
1513
  transition: all var(--transition-fast) var(--ease-spring);
1208
1514
  }
1209
1515
 
1210
- .session-item-delete {
1211
- border: 1px solid rgba(189, 70, 68, 0.35);
1212
- background: rgba(189, 70, 68, 0.08);
1213
- color: #b74545;
1214
- width: 28px;
1215
- height: 28px;
1216
- border-radius: 8px;
1217
- display: inline-flex;
1218
- align-items: center;
1219
- justify-content: center;
1220
- cursor: pointer;
1221
- flex-shrink: 0;
1222
- transition: all var(--transition-fast) var(--ease-spring);
1223
- }
1224
-
1225
1516
  .session-item-copy:hover {
1226
1517
  border-color: rgba(70, 86, 110, 0.7);
1227
1518
  background: rgba(70, 86, 110, 0.16);
@@ -1229,35 +1520,17 @@
1229
1520
  transform: translateY(-1px);
1230
1521
  }
1231
1522
 
1232
- .session-item-delete:hover {
1233
- border-color: rgba(189, 70, 68, 0.7);
1234
- background: rgba(189, 70, 68, 0.18);
1235
- color: #9f3b3b;
1236
- transform: translateY(-1px);
1237
- }
1238
-
1239
1523
  .session-item-copy:disabled {
1240
1524
  opacity: 0.5;
1241
1525
  cursor: not-allowed;
1242
1526
  transform: none;
1243
1527
  }
1244
1528
 
1245
- .session-item-delete:disabled {
1246
- opacity: 0.5;
1247
- cursor: not-allowed;
1248
- transform: none;
1249
- }
1250
-
1251
1529
  .session-item-copy svg {
1252
1530
  width: 16px;
1253
1531
  height: 16px;
1254
1532
  }
1255
1533
 
1256
- .session-item-delete svg {
1257
- width: 16px;
1258
- height: 16px;
1259
- }
1260
-
1261
1534
  .session-item-sub {
1262
1535
  font-size: var(--font-size-caption);
1263
1536
  color: var(--color-text-tertiary);
@@ -1286,26 +1559,37 @@
1286
1559
  white-space: nowrap;
1287
1560
  }
1288
1561
 
1289
- .session-item-snippet {
1290
- color: var(--color-text-secondary);
1291
- white-space: normal;
1292
- background: rgba(210, 107, 90, 0.08);
1293
- border-radius: 8px;
1294
- padding: 6px 8px;
1295
- border: 1px solid rgba(210, 107, 90, 0.15);
1296
- }
1297
-
1298
- .session-preview {
1299
- border: 1px solid var(--color-border-soft);
1300
- border-radius: var(--radius-xl);
1301
- background: linear-gradient(to bottom, var(--color-surface-elevated) 0%, rgba(255, 255, 255, 0.96) 100%);
1302
- box-shadow: var(--shadow-card);
1562
+ .session-item-snippet {
1563
+ color: var(--color-text-secondary);
1564
+ white-space: nowrap;
1565
+ overflow: hidden;
1566
+ text-overflow: ellipsis;
1567
+ display: block;
1568
+ max-width: 100%;
1569
+ background: rgba(210, 107, 90, 0.08);
1570
+ border-radius: 8px;
1571
+ padding: 6px 8px;
1572
+ border: 1px solid rgba(210, 107, 90, 0.15);
1573
+ }
1574
+
1575
+ .session-preview {
1576
+ border: 1px solid var(--color-border-soft);
1577
+ border-radius: var(--radius-xl);
1578
+ background: linear-gradient(to bottom, var(--color-surface-elevated) 0%, rgba(255, 255, 255, 0.96) 100%);
1579
+ box-shadow: var(--shadow-card);
1303
1580
  min-height: 0;
1304
1581
  max-height: none;
1305
1582
  height: 100%;
1306
1583
  display: flex;
1307
1584
  flex-direction: column;
1308
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);
1309
1593
  }
1310
1594
 
1311
1595
  .session-preview-scroll {
@@ -1383,15 +1667,16 @@
1383
1667
  opacity: 0.7;
1384
1668
  }
1385
1669
 
1386
- .session-actions {
1387
- display: flex;
1388
- align-items: center;
1389
- gap: 8px;
1390
- flex-shrink: 0;
1391
- margin-left: 0;
1392
- flex-wrap: wrap;
1393
- justify-content: flex-end;
1394
- }
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
+ }
1395
1680
 
1396
1681
  .session-preview-body {
1397
1682
  flex: 1;
@@ -1495,7 +1780,7 @@
1495
1780
  box-shadow: inset 0 0 0 6px rgba(255, 255, 255, 0.7);
1496
1781
  }
1497
1782
 
1498
- @media (max-width: 900px) {
1783
+ @media (max-width: 1100px) {
1499
1784
  .session-layout {
1500
1785
  grid-template-columns: 1fr;
1501
1786
  height: auto;
@@ -1510,6 +1795,10 @@
1510
1795
  justify-content: flex-start;
1511
1796
  }
1512
1797
 
1798
+ .session-toolbar-footer {
1799
+ justify-content: flex-start;
1800
+ }
1801
+
1513
1802
  .session-list {
1514
1803
  position: static;
1515
1804
  max-height: 300px;
@@ -1520,6 +1809,9 @@
1520
1809
  min-height: 360px;
1521
1810
  max-height: none;
1522
1811
  height: auto;
1812
+ position: relative;
1813
+ transform: none;
1814
+ box-shadow: var(--shadow-card);
1523
1815
  }
1524
1816
 
1525
1817
  .session-preview-header {
@@ -1530,6 +1822,10 @@
1530
1822
  .session-actions {
1531
1823
  justify-content: flex-start;
1532
1824
  }
1825
+
1826
+ .session-preview.active {
1827
+ box-shadow: var(--shadow-float);
1828
+ }
1533
1829
  }
1534
1830
 
1535
1831
  @media (max-width: 520px) {
@@ -2221,11 +2517,43 @@
2221
2517
  outline-offset: 2px;
2222
2518
  }
2223
2519
 
2520
+ @media (max-width: 960px) {
2521
+ .container {
2522
+ padding: 12px;
2523
+ }
2524
+ .app-shell {
2525
+ grid-template-columns: 1fr;
2526
+ }
2527
+ .side-rail {
2528
+ display: none;
2529
+ }
2530
+ .main-panel {
2531
+ padding: var(--spacing-sm) var(--spacing-sm);
2532
+ border-radius: 14px;
2533
+ }
2534
+ .top-tabs {
2535
+ display: grid !important;
2536
+ grid-template-columns: repeat(2, minmax(0, 1fr));
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
+ }
2546
+ }
2547
+
2224
2548
  @media (max-width: 720px) {
2225
2549
  .main-title {
2226
2550
  font-size: 40px;
2227
2551
  }
2228
2552
 
2553
+ .hero-title {
2554
+ font-size: 32px;
2555
+ }
2556
+
2229
2557
  .subtitle {
2230
2558
  font-size: var(--font-size-secondary);
2231
2559
  margin-bottom: 16px;
@@ -2235,38 +2563,372 @@
2235
2563
  flex-direction: column;
2236
2564
  gap: 6px;
2237
2565
  }
2566
+
2567
+ .status-strip {
2568
+ flex-direction: row;
2569
+ flex-wrap: wrap;
2570
+ }
2571
+
2572
+ .status-chip {
2573
+ flex: 1 1 100%;
2574
+ }
2575
+ }
2576
+
2577
+ @media (max-width: 540px) {
2578
+ body {
2579
+ padding: var(--spacing-md) var(--spacing-sm);
2580
+ }
2581
+ .container {
2582
+ padding: 0 var(--spacing-sm) var(--spacing-md);
2583
+ }
2584
+ .hero-title {
2585
+ font-size: 32px;
2586
+ }
2587
+ .hero-subtitle {
2588
+ font-size: var(--font-size-secondary);
2589
+ }
2590
+ .top-tabs {
2591
+ grid-template-columns: repeat(1, minmax(0, 1fr));
2592
+ }
2593
+ .main-panel {
2594
+ padding: var(--spacing-sm);
2595
+ }
2596
+ .card {
2597
+ padding: 12px;
2598
+ }
2599
+ .session-layout {
2600
+ grid-template-columns: 1fr;
2601
+ height: auto;
2602
+ min-height: 0;
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
+ }
2238
2723
  }
2239
2724
  </style>
2240
2725
  </head>
2241
2726
  <body>
2242
2727
  <div id="app" class="container" v-cloak>
2243
- <!-- 主标题 -->
2244
- <h1 v-if="!sessionStandalone" class="main-title">
2245
- Codex<br>
2246
- <span class="accent">Mate.</span>
2247
- </h1>
2248
- <p v-if="!sessionStandalone" class="subtitle">本地配置中枢,统一管理 Codex / Claude Code / OpenClaw / 会话。</p>
2249
-
2250
- <!-- 模式切换器 -->
2251
- <div v-if="!sessionStandalone" class="segmented-control">
2252
- <button :class="['segment', { active: configMode === 'codex' }]" @click="switchConfigMode('codex')">
2253
- Codex 配置
2254
- </button>
2255
- <button :class="['segment', { active: configMode === 'claude' }]" @click="switchConfigMode('claude')">
2256
- Claude Code 配置
2257
- </button>
2258
- <button :class="['segment', { active: configMode === 'openclaw' }]" @click="switchConfigMode('openclaw')">
2259
- OpenClaw 配置
2260
- </button>
2261
- <button :class="['segment', { active: configMode === 'sessions' }]" @click="switchConfigMode('sessions')">
2262
- 会话浏览
2263
- </button>
2728
+ <div class="hero" v-if="!sessionStandalone">
2729
+ <div class="hero-title">
2730
+ Codex <span class="accent">Mate.</span>
2731
+ </div>
2732
+ <div class="hero-subtitle">
2733
+ 本地配置中枢,统一管理 Codex / Claude Code / OpenClaw / 会话。
2734
+ </div>
2735
+ </div>
2736
+
2737
+ <div v-if="!sessionStandalone" class="top-tabs" role="tablist" aria-label="主导航">
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"
2745
+ :class="{ active: mainTab === 'config' && configMode === 'codex' }"
2746
+ @click="switchConfigMode('codex')">Codex 配置</button>
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"
2754
+ :class="{ active: mainTab === 'config' && configMode === 'claude' }"
2755
+ @click="switchConfigMode('claude')">Claude Code 配置</button>
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"
2763
+ :class="{ active: mainTab === 'config' && configMode === 'openclaw' }"
2764
+ @click="switchConfigMode('openclaw')">OpenClaw 配置</button>
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"
2772
+ :class="{ active: mainTab === 'sessions' }"
2773
+ @click="switchMainTab('sessions')">会话浏览</button>
2264
2774
  </div>
2265
2775
 
2266
- <!-- 内容包裹器 - 稳定布局 -->
2267
- <div class="content-wrapper">
2268
- <!-- Codex 配置模式 -->
2269
- <div v-show="configMode === 'codex'" class="mode-content">
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>
2855
+ <main class="main-panel">
2856
+ <div class="panel-header" v-if="!sessionStandalone">
2857
+ <h1 class="main-title">
2858
+ {{ mainTab === 'config' ? '配置中心' : '会话浏览' }}
2859
+ </h1>
2860
+ <p class="subtitle" v-if="mainTab === 'config'">
2861
+ 本地配置中枢,统一管理 Codex / Claude Code / OpenClaw。
2862
+ </p>
2863
+ <p class="subtitle" v-else>
2864
+ 浏览、导出或独立查看 Codex / Claude 会话记录。
2865
+ </p>
2866
+ </div>
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
+
2911
+ <div v-if="false && mainTab === 'config' && !sessionStandalone" class="config-subtabs">
2912
+ <button :class="['config-subtab', { active: configMode === 'codex' }]" @click="switchConfigMode('codex')">
2913
+ Codex 配置
2914
+ </button>
2915
+ <button :class="['config-subtab', { active: configMode === 'claude' }]" @click="switchConfigMode('claude')">
2916
+ Claude Code 配置
2917
+ </button>
2918
+ <button :class="['config-subtab', { active: configMode === 'openclaw' }]" @click="switchConfigMode('openclaw')">
2919
+ OpenClaw 配置
2920
+ </button>
2921
+ </div>
2922
+
2923
+ <!-- 内容包裹器 - 稳定布局 -->
2924
+ <div class="content-wrapper">
2925
+ <!-- Codex 配置模式 -->
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'">
2270
2932
  <!-- 添加提供商按钮 -->
2271
2933
  <button class="btn-add" @click="showAddModal = true" v-if="!loading && !initError">
2272
2934
  <svg class="icon" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2">
@@ -2320,22 +2982,14 @@
2320
2982
 
2321
2983
  <div class="selector-section">
2322
2984
  <div class="selector-header">
2323
- <span class="selector-title">最近使用</span>
2324
- <span v-if="recentLoading" class="selector-title">加载中...</span>
2985
+ <span class="selector-title">服务档位</span>
2325
2986
  </div>
2326
- <div v-if="recentConfigs.length === 0" class="recent-empty">
2327
- 暂无记录
2328
- </div>
2329
- <div v-else class="recent-list">
2330
- <button
2331
- v-for="item in recentConfigs"
2332
- :key="item.provider + '::' + item.model + '::' + (item.usedAt || '')"
2333
- class="recent-item"
2334
- @click="applyRecentConfig(item)"
2335
- :disabled="loading || !!initError">
2336
- <div class="recent-provider">{{ item.provider }}</div>
2337
- <div class="recent-model">{{ item.model }}</div>
2338
- </button>
2987
+ <select class="model-select" v-model="serviceTier" @change="onServiceTierChange">
2988
+ <option value="fast">fast(默认)</option>
2989
+ <option value="standard">standard</option>
2990
+ </select>
2991
+ <div class="config-template-hint">
2992
+ fast 会写入 <code>service_tier</code>。
2339
2993
  </div>
2340
2994
  </div>
2341
2995
 
@@ -2384,6 +3038,13 @@
2384
3038
  <path d="M13 2L3 14h7l-1 8 12-14h-7l-1-6z"/>
2385
3039
  </svg>
2386
3040
  </button>
3041
+ <button class="card-action-btn" :class="{ loading: providerShareLoading[provider.name] }" @click="copyProviderShareCommand(provider)" title="分享导入命令">
3042
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
3043
+ <path d="M4 12v7a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-7"/>
3044
+ <path d="M16 6l-4-4-4 4"/>
3045
+ <path d="M12 2v14"/>
3046
+ </svg>
3047
+ </button>
2387
3048
  <button class="card-action-btn" @click="openEditModal(provider)" title="编辑">
2388
3049
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
2389
3050
  <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
@@ -2402,8 +3063,13 @@
2402
3063
  </div>
2403
3064
  </div>
2404
3065
 
2405
- <!-- Claude Code 配置模式 -->
2406
- <div v-show="configMode === 'claude'" class="mode-content">
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'">
2407
3073
  <!-- 添加提供商按钮 -->
2408
3074
  <button class="btn-add" @click="openClaudeConfigModal" v-if="!loading && !initError">
2409
3075
  <svg class="icon" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2">
@@ -2419,7 +3085,16 @@
2419
3085
  <div class="selector-header">
2420
3086
  <span class="selector-title">模型</span>
2421
3087
  </div>
3088
+ <select
3089
+ v-if="claudeModelHasList"
3090
+ class="model-select"
3091
+ v-model="currentClaudeModel"
3092
+ @change="onClaudeModelChange"
3093
+ >
3094
+ <option v-for="model in claudeModelOptions" :key="model" :value="model">{{ model }}</option>
3095
+ </select>
2422
3096
  <input
3097
+ v-else
2423
3098
  class="model-input"
2424
3099
  v-model="currentClaudeModel"
2425
3100
  @blur="onClaudeModelChange"
@@ -2431,6 +3106,15 @@
2431
3106
  </div>
2432
3107
  </div>
2433
3108
 
3109
+ <div class="selector-section">
3110
+ <div class="selector-header">
3111
+ <span class="selector-title">配置健康检查</span>
3112
+ </div>
3113
+ <button class="btn-tool" @click="runHealthCheck" :disabled="healthCheckLoading || loading || !!initError">
3114
+ {{ healthCheckLoading ? '检查中...' : '运行检查' }}
3115
+ </button>
3116
+ </div>
3117
+
2434
3118
  <div class="card-list">
2435
3119
  <div v-for="(config, name) in claudeConfigs" :key="name"
2436
3120
  :class="['card', { active: currentClaudeConfig === name }]"
@@ -2446,6 +3130,9 @@
2446
3130
  <span :class="['pill', config.hasKey ? 'configured' : 'empty']">
2447
3131
  {{ config.hasKey ? '已配置' : '未配置' }}
2448
3132
  </span>
3133
+ <span v-if="claudeSpeedResults[name]" :class="['latency', claudeSpeedResults[name].ok ? 'ok' : 'error']">
3134
+ {{ formatLatency(claudeSpeedResults[name]) }}
3135
+ </span>
2449
3136
  <div class="card-actions" @click.stop>
2450
3137
  <button class="card-action-btn" @click="openEditConfigModal(name)" title="编辑">
2451
3138
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@@ -2453,6 +3140,13 @@
2453
3140
  <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
2454
3141
  </svg>
2455
3142
  </button>
3143
+ <button class="card-action-btn" :class="{ loading: claudeShareLoading[name] }" @click="copyClaudeShareCommand(name)" title="分享导入命令" aria-label="Share import command">
3144
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
3145
+ <path d="M4 12v7a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-7"/>
3146
+ <path d="M16 6l-4-4-4 4"/>
3147
+ <path d="M12 2v14"/>
3148
+ </svg>
3149
+ </button>
2456
3150
  <button class="card-action-btn delete" @click="deleteClaudeConfig(name)" title="删除">
2457
3151
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
2458
3152
  <path d="M3 6h18"/>
@@ -2465,8 +3159,13 @@
2465
3159
  </div>
2466
3160
  </div>
2467
3161
 
2468
- <!-- OpenClaw 配置模式 -->
2469
- <div v-show="configMode === 'openclaw'" class="mode-content">
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'">
2470
3169
  <button class="btn-add" @click="openOpenclawAddModal" v-if="!loading && !initError">
2471
3170
  <svg class="icon" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2">
2472
3171
  <path d="M10 4v12M4 10h12"/>
@@ -2539,36 +3238,40 @@
2539
3238
  </div>
2540
3239
  </div>
2541
3240
 
2542
- <!-- 会话浏览模式 -->
2543
- <div v-show="configMode === 'sessions'" class="mode-content">
2544
- <div v-if="sessionStandalone" class="session-standalone-page">
2545
- <div v-if="sessionStandaloneLoading" class="state-message">
2546
- 加载中...
2547
- </div>
2548
- <div v-else-if="sessionStandaloneError" class="state-message error">
2549
- {{ sessionStandaloneError }}
2550
- </div>
2551
- <div v-else>
2552
- <div class="session-standalone-title">
2553
- {{ sessionStandaloneTitle }}
2554
- <span v-if="sessionStandaloneSourceLabel"> · {{ sessionStandaloneSourceLabel }}</span>
2555
- </div>
2556
- <pre class="session-standalone-text">{{ sessionStandaloneText }}</pre>
2557
- </div>
2558
- </div>
2559
-
2560
- <div v-else>
2561
- <div v-if="!sessionStandalone" class="selector-section">
2562
- <div class="selector-header">
2563
- <span class="selector-title">会话来源</span>
2564
- </div>
3241
+ <!-- 会话浏览模式 -->
3242
+ <div
3243
+ v-show="mainTab === 'sessions'"
3244
+ class="mode-content"
3245
+ id="panel-sessions"
3246
+ role="tabpanel"
3247
+ :aria-labelledby="'tab-sessions'">
3248
+ <div v-if="sessionStandalone" class="session-standalone-page">
3249
+ <div v-if="sessionStandaloneLoading" class="state-message">
3250
+ 加载中...
3251
+ </div>
3252
+ <div v-else-if="sessionStandaloneError" class="state-message error">
3253
+ {{ sessionStandaloneError }}
3254
+ </div>
3255
+ <div v-else>
3256
+ <div class="session-standalone-title">
3257
+ {{ sessionStandaloneTitle }}
3258
+ <span v-if="sessionStandaloneSourceLabel"> · {{ sessionStandaloneSourceLabel }}</span>
3259
+ </div>
3260
+ <pre class="session-standalone-text">{{ sessionStandaloneText }}</pre>
3261
+ </div>
3262
+ </div>
3263
+
3264
+ <div v-else>
3265
+ <div v-if="!sessionStandalone" class="selector-section">
3266
+ <div class="selector-header">
3267
+ <span class="selector-title">会话来源</span>
3268
+ </div>
2565
3269
  <div class="session-toolbar">
2566
3270
  <div class="session-toolbar-group">
2567
3271
  <select class="session-source-select" v-model="sessionFilterSource" @change="onSessionSourceChange" :disabled="sessionsLoading">
2568
- <option value="all">全部(Codex + Claude)</option>
2569
- <option value="codex">仅 Codex</option>
2570
- <option value="claude">仅 Claude Code</option>
2571
- </select>
3272
+ <option value="codex">Codex</option>
3273
+ <option value="claude">Claude Code</option>
3274
+ </select>
2572
3275
  <select
2573
3276
  class="session-path-select"
2574
3277
  v-model="sessionPathFilter"
@@ -2617,10 +3320,15 @@
2617
3320
  </button>
2618
3321
  </div>
2619
3322
  </div>
2620
- <div class="session-hint">
2621
- 关键词检索仅 Codex 可用;<br>
2622
- 角色/时间筛选暂不可用;<br>
2623
- 仅支持来源与路径筛选,右侧仅查看/导出。
3323
+ <div class="session-toolbar-footer">
3324
+ <label class="quick-option">
3325
+ <input
3326
+ type="checkbox"
3327
+ v-model="sessionResumeWithYolo"
3328
+ @change="onSessionResumeYoloChange"
3329
+ >
3330
+ 复制恢复命令附带 --yolo
3331
+ </label>
2624
3332
  </div>
2625
3333
  </div>
2626
3334
 
@@ -2637,13 +3345,13 @@
2637
3345
  <div
2638
3346
  v-for="session in sessionsList"
2639
3347
  :key="session.source + '-' + session.sessionId + '-' + session.filePath"
2640
- :class="[
2641
- 'session-item',
2642
- {
2643
- active: activeSession && getSessionExportKey(activeSession) === getSessionExportKey(session)
2644
- }
2645
- ]"
2646
- @click="selectSession(session)">
3348
+ :class="[
3349
+ 'session-item',
3350
+ {
3351
+ active: activeSession && getSessionExportKey(activeSession) === getSessionExportKey(session)
3352
+ }
3353
+ ]"
3354
+ @click="selectSession(session)">
2647
3355
  <div class="session-item-header">
2648
3356
  <div class="session-item-main">
2649
3357
  <div class="session-item-title">{{ session.title || session.sessionId }}</div>
@@ -2661,21 +3369,6 @@
2661
3369
  <path d="M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2"></path>
2662
3370
  </svg>
2663
3371
  </button>
2664
- <button
2665
- v-if="isDeleteAvailable(session)"
2666
- class="session-item-delete"
2667
- @click.stop="deleteSession(session)"
2668
- :disabled="sessionsLoading || sessionDeleting[getSessionExportKey(session)]"
2669
- aria-label="删除会话"
2670
- title="删除会话">
2671
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
2672
- <path d="M3 6h18"></path>
2673
- <path d="M8 6V4h8v2"></path>
2674
- <path d="M19 6l-1 14H6L5 6"></path>
2675
- <path d="M10 11v6"></path>
2676
- <path d="M14 11v6"></path>
2677
- </svg>
2678
- </button>
2679
3372
  </div>
2680
3373
  </div>
2681
3374
  <div class="session-item-meta">
@@ -2690,7 +3383,7 @@
2690
3383
  </div>
2691
3384
  </div>
2692
3385
 
2693
- <div class="session-preview">
3386
+ <div :class="['session-preview', { active: !!activeSession }]">
2694
3387
  <template v-if="activeSession">
2695
3388
  <div class="session-preview-scroll">
2696
3389
  <div class="session-preview-header">
@@ -2704,7 +3397,7 @@
2704
3397
  <span class="session-preview-meta-item">{{ activeSession.cwd }}</span>
2705
3398
  </div>
2706
3399
  </div>
2707
- <div v-if="!sessionStandalone" class="session-actions">
3400
+ <div v-if="!sessionStandalone" class="session-actions">
2708
3401
  <button class="btn-session-refresh" @click="loadActiveSessionDetail" :disabled="sessionDetailLoading || !activeSession">
2709
3402
  {{ sessionDetailLoading ? '加载中...' : '刷新内容' }}
2710
3403
  </button>
@@ -2770,14 +3463,14 @@
2770
3463
  </div>
2771
3464
  </template>
2772
3465
 
2773
- <div v-else class="session-preview-empty">
2774
- <span v-if="sessionStandaloneError">{{ sessionStandaloneError }}</span>
2775
- <span v-else>请先在左侧选择一个会话</span>
2776
- </div>
2777
- </div>
2778
- </div>
2779
- </div>
2780
- </div>
3466
+ <div v-else class="session-preview-empty">
3467
+ <span v-if="sessionStandaloneError">{{ sessionStandaloneError }}</span>
3468
+ <span v-else>请先在左侧选择一个会话</span>
3469
+ </div>
3470
+ </div>
3471
+ </div>
3472
+ </div>
3473
+ </div>
2781
3474
 
2782
3475
  <!-- 加载状态 -->
2783
3476
  <div v-if="loading" class="state-message">
@@ -2790,6 +3483,9 @@
2790
3483
  </div>
2791
3484
  </div>
2792
3485
 
3486
+ </main>
3487
+ </div>
3488
+
2793
3489
  <!-- 添加提供商模态框 -->
2794
3490
  <div v-if="showAddModal" class="modal-overlay" @click.self="showAddModal = false">
2795
3491
  <div class="modal">
@@ -3276,2653 +3972,6 @@
3276
3972
  <div v-if="message" :class="['toast', messageType]">{{ message }}</div>
3277
3973
  </div>
3278
3974
 
3279
- <script>
3280
- const { createApp } = Vue;
3281
- const API_BASE = (location && location.origin && location.origin !== 'null')
3282
- ? location.origin
3283
- : 'http://localhost:3737';
3284
- const DEFAULT_OPENCLAW_TEMPLATE = `{
3285
- // OpenClaw config (JSON5)
3286
- agent: {
3287
- model: "gpt-4.1"
3288
- },
3289
- agents: {
3290
- defaults: {
3291
- workspace: "~/.openclaw/workspace"
3292
- }
3293
- }
3294
- }`;
3295
-
3296
- async function api(action, params = {}) {
3297
- const res = await fetch(`${API_BASE}/api`, {
3298
- method: 'POST',
3299
- headers: { 'Content-Type': 'application/json' },
3300
- body: JSON.stringify({ action, params })
3301
- });
3302
- return await res.json();
3303
- }
3304
-
3305
- const app = createApp({
3306
- data() {
3307
- return {
3308
- configMode: 'codex',
3309
- currentProvider: '',
3310
- currentModel: '',
3311
- providersList: [],
3312
- models: [],
3313
- codexModelsLoading: false,
3314
- modelsSource: 'remote',
3315
- modelsHasCurrent: true,
3316
- claudeModels: [],
3317
- claudeModelsSource: 'idle',
3318
- claudeModelsHasCurrent: true,
3319
- claudeModelsLoading: false,
3320
- loading: true,
3321
- initError: '',
3322
- message: '',
3323
- messageType: '',
3324
- showAddModal: false,
3325
- showEditModal: false,
3326
- showModelModal: false,
3327
- showModelListModal: false,
3328
- showClaudeConfigModal: false,
3329
- showEditConfigModal: false,
3330
- showOpenclawConfigModal: false,
3331
- showConfigTemplateModal: false,
3332
- showAgentsModal: false,
3333
- configTemplateContent: '',
3334
- configTemplateApplying: false,
3335
- agentsContent: '',
3336
- agentsPath: '',
3337
- agentsExists: false,
3338
- agentsLineEnding: '\n',
3339
- agentsLoading: false,
3340
- agentsSaving: false,
3341
- agentsContext: 'codex',
3342
- agentsModalTitle: 'AGENTS.md 编辑器',
3343
- agentsModalHint: '保存后会写入目标 AGENTS.md(与 config.toml 同级)。',
3344
- sessionsList: [],
3345
- sessionsLoading: false,
3346
- sessionFilterSource: 'all',
3347
- sessionPathFilter: '',
3348
- sessionQuery: '',
3349
- sessionRoleFilter: 'all',
3350
- sessionTimePreset: 'all',
3351
- sessionPathOptions: [],
3352
- sessionPathOptionsLoading: false,
3353
- sessionPathOptionsMap: {
3354
- all: [],
3355
- codex: [],
3356
- claude: []
3357
- },
3358
- sessionPathOptionsLoadedMap: {
3359
- all: false,
3360
- codex: false,
3361
- claude: false
3362
- },
3363
- sessionPathRequestSeq: 0,
3364
- sessionExporting: {},
3365
- sessionCloning: {},
3366
- sessionDeleting: {},
3367
- activeSession: null,
3368
- activeSessionMessages: [],
3369
- activeSessionDetailError: '',
3370
- activeSessionDetailClipped: false,
3371
- sessionDetailLoading: false,
3372
- sessionDetailRequestSeq: 0,
3373
- sessionStandalone: false,
3374
- sessionStandaloneError: '',
3375
- sessionStandaloneText: '',
3376
- sessionStandaloneTitle: '',
3377
- sessionStandaloneSourceLabel: '',
3378
- sessionStandaloneLoading: false,
3379
- sessionStandaloneRequestSeq: 0,
3380
- speedResults: {},
3381
- speedLoading: {},
3382
- newProvider: { name: '', url: '', key: '' },
3383
- editingProvider: { name: '', url: '', key: '' },
3384
- newModelName: '',
3385
- currentClaudeConfig: '',
3386
- currentClaudeModel: '',
3387
- editingConfig: { name: '', apiKey: '', baseUrl: '', model: '' },
3388
- claudeConfigs: {
3389
- '智谱GLM': {
3390
- apiKey: '',
3391
- baseUrl: 'https://open.bigmodel.cn/api/anthropic',
3392
- model: 'glm-4.7',
3393
- hasKey: false
3394
- }
3395
- },
3396
- newClaudeConfig: {
3397
- name: '',
3398
- apiKey: '',
3399
- baseUrl: 'https://open.bigmodel.cn/api/anthropic',
3400
- model: 'glm-4.7'
3401
- },
3402
- currentOpenclawConfig: '',
3403
- openclawConfigs: {
3404
- '默认配置': {
3405
- content: DEFAULT_OPENCLAW_TEMPLATE
3406
- }
3407
- },
3408
- openclawEditing: { name: '', content: '', lockName: false },
3409
- openclawEditorTitle: '添加 OpenClaw 配置',
3410
- openclawConfigPath: '',
3411
- openclawConfigExists: false,
3412
- openclawLineEnding: '\n',
3413
- openclawFileLoading: false,
3414
- openclawSaving: false,
3415
- openclawApplying: false,
3416
- openclawWorkspaceFileName: 'SOUL.md',
3417
- agentsWorkspaceFileName: '',
3418
- openclawStructured: {
3419
- agentPrimary: '',
3420
- agentFallbacks: [],
3421
- workspace: '',
3422
- timeout: '',
3423
- contextTokens: '',
3424
- maxConcurrent: '',
3425
- envItems: [],
3426
- toolsProfile: 'default',
3427
- toolsAllow: [],
3428
- toolsDeny: []
3429
- },
3430
- openclawQuick: {
3431
- providerName: '',
3432
- baseUrl: '',
3433
- apiKey: '',
3434
- apiType: 'openai-responses',
3435
- modelId: '',
3436
- modelName: '',
3437
- contextWindow: '',
3438
- maxTokens: '',
3439
- setPrimary: true,
3440
- overrideProvider: true,
3441
- overrideModels: true,
3442
- showKey: false
3443
- },
3444
- openclawAgentsList: [],
3445
- openclawProviders: [],
3446
- openclawMissingProviders: [],
3447
- recentConfigs: [],
3448
- recentLoading: false,
3449
- healthCheckLoading: false,
3450
- healthCheckResult: null,
3451
- healthCheckRemote: false
3452
- }
3453
- },
3454
- mounted() {
3455
- this.initSessionStandalone();
3456
- const savedConfigs = localStorage.getItem('claudeConfigs');
3457
- if (savedConfigs) {
3458
- try {
3459
- this.claudeConfigs = JSON.parse(savedConfigs);
3460
- for (const [name, config] of Object.entries(this.claudeConfigs)) {
3461
- if (config.apiKey && config.apiKey.includes('****')) {
3462
- config.apiKey = '';
3463
- config.hasKey = false;
3464
- }
3465
- }
3466
- localStorage.setItem('claudeConfigs', JSON.stringify(this.claudeConfigs));
3467
-
3468
- const configNames = Object.keys(this.claudeConfigs);
3469
- if (configNames.length > 0) {
3470
- this.currentClaudeConfig = configNames[0];
3471
- }
3472
- } catch (e) {
3473
- console.error('加载 Claude 配置失败:', e);
3474
- }
3475
- }
3476
- if (!this.currentClaudeConfig) {
3477
- const configNames = Object.keys(this.claudeConfigs);
3478
- if (configNames.length > 0) {
3479
- this.currentClaudeConfig = configNames[0];
3480
- }
3481
- }
3482
- this.syncClaudeModelFromConfig();
3483
- const savedOpenclawConfigs = localStorage.getItem('openclawConfigs');
3484
- if (savedOpenclawConfigs) {
3485
- try {
3486
- this.openclawConfigs = JSON.parse(savedOpenclawConfigs);
3487
- const configNames = Object.keys(this.openclawConfigs);
3488
- if (configNames.length > 0) {
3489
- this.currentOpenclawConfig = configNames[0];
3490
- }
3491
- } catch (e) {
3492
- console.error('加载 OpenClaw 配置失败:', e);
3493
- }
3494
- } else {
3495
- const configNames = Object.keys(this.openclawConfigs);
3496
- if (configNames.length > 0) {
3497
- this.currentOpenclawConfig = configNames[0];
3498
- }
3499
- }
3500
- this.loadAll();
3501
- },
3502
-
3503
- computed: {
3504
- isSessionQueryEnabled() {
3505
- return this.sessionFilterSource === 'codex';
3506
- },
3507
- sessionQueryPlaceholder() {
3508
- return this.isSessionQueryEnabled ? '关键词检索' : '仅 Codex 支持关键词检索';
3509
- }
3510
- },
3511
- methods: {
3512
- async loadAll() {
3513
- this.loading = true;
3514
- this.initError = '';
3515
- try {
3516
- const statusRes = await api('status');
3517
- const listRes = await api('list');
3518
-
3519
- if (statusRes.error) {
3520
- this.initError = statusRes.error;
3521
- } else {
3522
- this.currentProvider = statusRes.provider;
3523
- this.currentModel = statusRes.model;
3524
- this.providersList = listRes.providers;
3525
- await this.loadModelsForProvider(this.currentProvider);
3526
- if (statusRes.configReady === false) {
3527
- this.showMessage(statusRes.configNotice || '未检测到 config.toml,已加载默认模板。请在模板编辑器确认后创建。', 'info');
3528
- }
3529
- if (statusRes.initNotice) {
3530
- this.showMessage(statusRes.initNotice, 'info');
3531
- }
3532
- this.maybeShowStarPrompt();
3533
- }
3534
- await this.loadRecentConfigs();
3535
- } catch (e) {
3536
- this.initError = '连接失败: ' + e.message;
3537
- } finally {
3538
- this.loading = false;
3539
- }
3540
- },
3541
-
3542
- async loadModelsForProvider(providerName) {
3543
- this.codexModelsLoading = true;
3544
- if (!providerName) {
3545
- this.models = [];
3546
- this.modelsSource = 'unlimited';
3547
- this.modelsHasCurrent = true;
3548
- this.codexModelsLoading = false;
3549
- return;
3550
- }
3551
- try {
3552
- const res = await api('models', { provider: providerName });
3553
- if (res.unlimited) {
3554
- this.models = [];
3555
- this.modelsSource = 'unlimited';
3556
- this.modelsHasCurrent = true;
3557
- return;
3558
- }
3559
- if (res.error) {
3560
- this.showMessage('模型列表获取失败: ' + res.error, 'error');
3561
- this.models = [];
3562
- this.modelsSource = 'error';
3563
- this.modelsHasCurrent = true;
3564
- return;
3565
- }
3566
- const list = Array.isArray(res.models) ? res.models : [];
3567
- this.models = list;
3568
- this.modelsSource = res.source || 'remote';
3569
- this.modelsHasCurrent = !!this.currentModel && list.includes(this.currentModel);
3570
- } catch (e) {
3571
- this.showMessage('模型列表获取失败: ' + e.message, 'error');
3572
- this.models = [];
3573
- this.modelsSource = 'error';
3574
- this.modelsHasCurrent = true;
3575
- } finally {
3576
- this.codexModelsLoading = false;
3577
- }
3578
- },
3579
-
3580
- getCurrentClaudeConfig() {
3581
- if (!this.currentClaudeConfig) return null;
3582
- return this.claudeConfigs[this.currentClaudeConfig] || null;
3583
- },
3584
-
3585
- syncClaudeModelFromConfig() {
3586
- const config = this.getCurrentClaudeConfig();
3587
- this.currentClaudeModel = config && config.model ? config.model : '';
3588
- },
3589
-
3590
- refreshClaudeModelContext() {
3591
- this.syncClaudeModelFromConfig();
3592
- this.loadClaudeModels();
3593
- },
3594
-
3595
- resetClaudeModelsState() {
3596
- this.claudeModels = [];
3597
- this.claudeModelsSource = 'idle';
3598
- this.claudeModelsHasCurrent = true;
3599
- this.claudeModelsLoading = false;
3600
- },
3601
-
3602
- updateClaudeModelsCurrent() {
3603
- const currentModel = (this.currentClaudeModel || '').trim();
3604
- this.claudeModelsHasCurrent = !!currentModel && this.claudeModels.includes(currentModel);
3605
- },
3606
-
3607
- async loadClaudeModels() {
3608
- const config = this.getCurrentClaudeConfig();
3609
- const baseUrl = (config.baseUrl || '').trim();
3610
- const apiKey = (config.apiKey || '').trim();
3611
-
3612
- if (!baseUrl) {
3613
- this.resetClaudeModelsState();
3614
- return;
3615
- }
3616
-
3617
- this.claudeModelsLoading = true;
3618
- try {
3619
- const res = await api('models-by-url', { baseUrl, apiKey });
3620
- if (res.unlimited) {
3621
- this.claudeModels = [];
3622
- this.claudeModelsSource = 'unlimited';
3623
- this.claudeModelsHasCurrent = true;
3624
- return;
3625
- }
3626
- if (res.error) {
3627
- this.showMessage('模型列表获取失败: ' + res.error, 'error');
3628
- this.claudeModels = [];
3629
- this.claudeModelsSource = 'error';
3630
- this.claudeModelsHasCurrent = true;
3631
- return;
3632
- }
3633
- const list = Array.isArray(res.models) ? res.models : [];
3634
- this.claudeModels = list;
3635
- this.claudeModelsSource = res.source || 'remote';
3636
- this.updateClaudeModelsCurrent();
3637
- } catch (e) {
3638
- this.showMessage('模型列表获取失败: ' + e.message, 'error');
3639
- this.claudeModels = [];
3640
- this.claudeModelsSource = 'error';
3641
- this.claudeModelsHasCurrent = true;
3642
- } finally {
3643
- this.claudeModelsLoading = false;
3644
- }
3645
- },
3646
-
3647
- openClaudeConfigModal() {
3648
- this.showClaudeConfigModal = true;
3649
- },
3650
-
3651
- maybeShowStarPrompt() {
3652
- const storageKey = 'codexmateStarPrompted';
3653
- if (localStorage.getItem(storageKey)) {
3654
- return;
3655
- }
3656
- this.showMessage('如果 Codex Mate 对你有帮助,欢迎到 GitHub 点个 Star。', 'info');
3657
- localStorage.setItem(storageKey, '1');
3658
- },
3659
-
3660
- switchConfigMode(mode) {
3661
- this.configMode = mode;
3662
- if (mode === 'claude') {
3663
- this.refreshClaudeModelContext();
3664
- }
3665
- if (mode === 'sessions' && this.sessionsList.length === 0) {
3666
- this.loadSessions();
3667
- }
3668
- },
3669
-
3670
- getSessionStandaloneContext() {
3671
- try {
3672
- const url = new URL(window.location.href);
3673
- if (url.pathname !== '/session') {
3674
- return { requested: false, params: null, error: '' };
3675
- }
3676
-
3677
- const source = (url.searchParams.get('source') || '').trim().toLowerCase();
3678
- const sessionId = (url.searchParams.get('sessionId') || url.searchParams.get('id') || '').trim();
3679
- const filePath = (url.searchParams.get('filePath') || url.searchParams.get('path') || '').trim();
3680
- let error = '';
3681
- if (!source) {
3682
- error = '缺少 source 参数';
3683
- } else if (source !== 'codex' && source !== 'claude') {
3684
- error = 'source 仅支持 codex 或 claude';
3685
- }
3686
- if (!sessionId && !filePath) {
3687
- error = error ? `${error},还缺少 sessionId 或 filePath` : '缺少 sessionId 或 filePath 参数';
3688
- }
3689
-
3690
- if (error) {
3691
- return { requested: true, params: null, error };
3692
- }
3693
-
3694
- return {
3695
- requested: true,
3696
- params: {
3697
- source,
3698
- sessionId,
3699
- filePath
3700
- },
3701
- error: ''
3702
- };
3703
- } catch (_) {
3704
- return { requested: false, params: null, error: '' };
3705
- }
3706
- },
3707
-
3708
- initSessionStandalone() {
3709
- const context = this.getSessionStandaloneContext();
3710
- if (!context.requested) return;
3711
-
3712
- this.sessionStandalone = true;
3713
- this.configMode = 'sessions';
3714
-
3715
- if (context.error || !context.params) {
3716
- this.sessionStandaloneError = `会话链接参数不完整:${context.error || '参数解析失败'}`;
3717
- return;
3718
- }
3719
-
3720
- const sourceLabel = context.params.source === 'codex' ? 'Codex' : 'Claude Code';
3721
- this.activeSession = {
3722
- source: context.params.source,
3723
- sourceLabel,
3724
- sessionId: context.params.sessionId,
3725
- filePath: context.params.filePath,
3726
- title: context.params.sessionId || context.params.filePath || '会话'
3727
- };
3728
- this.activeSessionMessages = [];
3729
- this.activeSessionDetailError = '';
3730
- this.activeSessionDetailClipped = false;
3731
- this.sessionStandaloneError = '';
3732
- this.sessionStandaloneText = '';
3733
- this.sessionStandaloneTitle = this.activeSession.title || '会话';
3734
- this.sessionStandaloneSourceLabel = sourceLabel;
3735
- this.loadSessionStandalonePlain();
3736
- },
3737
-
3738
- buildSessionStandaloneUrl(session) {
3739
- if (!session) return '';
3740
- const source = typeof session.source === 'string' ? session.source.trim().toLowerCase() : '';
3741
- if (!source || (source !== 'codex' && source !== 'claude')) return '';
3742
- const sessionId = typeof session.sessionId === 'string' ? session.sessionId.trim() : '';
3743
- const filePath = typeof session.filePath === 'string' ? session.filePath.trim() : '';
3744
- if (!sessionId && !filePath) return '';
3745
- const origin = window.location.origin && window.location.origin !== 'null'
3746
- ? window.location.origin
3747
- : API_BASE;
3748
- const params = new URLSearchParams();
3749
- params.set('source', source);
3750
- if (sessionId) params.set('sessionId', sessionId);
3751
- if (filePath) params.set('filePath', filePath);
3752
- return `${origin}/session?${params.toString()}`;
3753
- },
3754
-
3755
- openSessionStandalone(session) {
3756
- const url = this.buildSessionStandaloneUrl(session);
3757
- if (!url) {
3758
- this.showMessage('当前会话无法生成新页链接', 'error');
3759
- return;
3760
- }
3761
- window.open(url, '_blank', 'noopener');
3762
- },
3763
-
3764
- getSessionExportKey(session) {
3765
- return `${session.source || 'unknown'}:${session.sessionId || ''}:${session.filePath || ''}`;
3766
- },
3767
-
3768
- isResumeCommandAvailable(session) {
3769
- if (!session) return false;
3770
- const source = String(session.source || '').trim().toLowerCase();
3771
- const sessionId = typeof session.sessionId === 'string' ? session.sessionId.trim() : '';
3772
- return source === 'codex' && !!sessionId;
3773
- },
3774
-
3775
- isCloneAvailable(session) {
3776
- if (!session) return false;
3777
- const source = String(session.source || '').trim().toLowerCase();
3778
- const sessionId = typeof session.sessionId === 'string' ? session.sessionId.trim() : '';
3779
- const filePath = typeof session.filePath === 'string' ? session.filePath.trim() : '';
3780
- return source === 'codex' && (!!sessionId || !!filePath);
3781
- },
3782
-
3783
- isDeleteAvailable(session) {
3784
- if (!session) return false;
3785
- const source = String(session.source || '').trim().toLowerCase();
3786
- if (source !== 'codex' && source !== 'claude') return false;
3787
- const sessionId = typeof session.sessionId === 'string' ? session.sessionId.trim() : '';
3788
- const filePath = typeof session.filePath === 'string' ? session.filePath.trim() : '';
3789
- return !!sessionId || !!filePath;
3790
- },
3791
-
3792
- buildResumeCommand(session) {
3793
- const sessionId = session && session.sessionId ? String(session.sessionId).trim() : '';
3794
- return `codex resume ${this.quoteResumeArg(sessionId)}`;
3795
- },
3796
-
3797
- quoteResumeArg(value) {
3798
- const text = typeof value === 'string' ? value : String(value || '');
3799
- if (!text) return "''";
3800
- if (/^[a-zA-Z0-9._-]+$/.test(text)) return text;
3801
- const escaped = text.replace(/'/g, "'\\''");
3802
- return `'${escaped}'`;
3803
- },
3804
-
3805
- fallbackCopyText(text) {
3806
- let textarea = null;
3807
- try {
3808
- textarea = document.createElement('textarea');
3809
- textarea.value = text;
3810
- textarea.setAttribute('readonly', '');
3811
- textarea.style.position = 'fixed';
3812
- textarea.style.top = '-9999px';
3813
- textarea.style.left = '-9999px';
3814
- textarea.style.opacity = '0';
3815
- document.body.appendChild(textarea);
3816
- textarea.select();
3817
- textarea.setSelectionRange(0, textarea.value.length);
3818
- return document.execCommand('copy');
3819
- } catch (e) {
3820
- return false;
3821
- } finally {
3822
- if (textarea && textarea.parentNode) {
3823
- textarea.parentNode.removeChild(textarea);
3824
- }
3825
- }
3826
- },
3827
-
3828
- copyAgentsContent() {
3829
- const text = typeof this.agentsContent === 'string' ? this.agentsContent : '';
3830
- if (!text) {
3831
- this.showMessage('没有可复制的内容', 'info');
3832
- return;
3833
- }
3834
- const ok = this.fallbackCopyText(text);
3835
- if (ok) {
3836
- this.showMessage('已复制 AGENTS.md 内容', 'success');
3837
- return;
3838
- }
3839
- this.showMessage('复制失败,请手动复制内容', 'error');
3840
- },
3841
-
3842
- async copyResumeCommand(session) {
3843
- if (!this.isResumeCommandAvailable(session)) {
3844
- this.showMessage('当前会话不支持生成恢复命令', 'error');
3845
- return;
3846
- }
3847
- const command = this.buildResumeCommand(session);
3848
- const ok = this.fallbackCopyText(command);
3849
- if (ok) {
3850
- this.showMessage('已复制恢复命令', 'success');
3851
- return;
3852
- }
3853
- try {
3854
- if (navigator.clipboard && window.isSecureContext) {
3855
- await navigator.clipboard.writeText(command);
3856
- this.showMessage('已复制恢复命令', 'success');
3857
- return;
3858
- }
3859
- } catch (e) {
3860
- // keep fallback failure message
3861
- }
3862
- this.showMessage('复制失败,请手动复制命令', 'error');
3863
- },
3864
-
3865
- async cloneSession(session) {
3866
- if (!this.isCloneAvailable(session)) {
3867
- this.showMessage('当前会话不支持克隆', 'error');
3868
- return;
3869
- }
3870
- const key = this.getSessionExportKey(session);
3871
- if (this.sessionCloning[key]) {
3872
- return;
3873
- }
3874
- this.sessionCloning[key] = true;
3875
- try {
3876
- const res = await api('clone-session', {
3877
- source: session.source,
3878
- sessionId: session.sessionId,
3879
- filePath: session.filePath
3880
- });
3881
- if (res.error) {
3882
- this.showMessage(res.error, 'error');
3883
- return;
3884
- }
3885
-
3886
- this.showMessage('会话已克隆', 'success');
3887
- await this.loadSessions();
3888
- if (res.sessionId) {
3889
- const matched = this.sessionsList.find(item => item.source === 'codex' && item.sessionId === res.sessionId);
3890
- if (matched) {
3891
- await this.selectSession(matched);
3892
- }
3893
- }
3894
- } catch (e) {
3895
- this.showMessage('克隆失败: ' + e.message, 'error');
3896
- } finally {
3897
- this.sessionCloning[key] = false;
3898
- }
3899
- },
3900
-
3901
- async deleteSession(session) {
3902
- if (!this.isDeleteAvailable(session)) {
3903
- this.showMessage('当前会话不支持删除', 'error');
3904
- return;
3905
- }
3906
- const key = this.getSessionExportKey(session);
3907
- if (this.sessionDeleting[key]) {
3908
- return;
3909
- }
3910
- this.sessionDeleting[key] = true;
3911
- try {
3912
- const res = await api('delete-session', {
3913
- source: session.source,
3914
- sessionId: session.sessionId,
3915
- filePath: session.filePath
3916
- });
3917
- if (res.error) {
3918
- this.showMessage(res.error, 'error');
3919
- return;
3920
- }
3921
- this.showMessage('会话已删除', 'success');
3922
- await this.loadSessions();
3923
- } catch (e) {
3924
- this.showMessage('删除失败: ' + e.message, 'error');
3925
- } finally {
3926
- this.sessionDeleting[key] = false;
3927
- }
3928
- },
3929
-
3930
- normalizeSessionPathValue(value) {
3931
- if (typeof value !== 'string') return '';
3932
- return value.trim();
3933
- },
3934
-
3935
- mergeSessionPathOptions(baseList = [], incomingList = []) {
3936
- const merged = [];
3937
- const seen = new Set();
3938
- const append = (items) => {
3939
- if (!Array.isArray(items)) return;
3940
- for (const item of items) {
3941
- const value = this.normalizeSessionPathValue(item);
3942
- if (!value) continue;
3943
- const key = value.toLowerCase();
3944
- if (seen.has(key)) continue;
3945
- seen.add(key);
3946
- merged.push(value);
3947
- }
3948
- };
3949
-
3950
- append(baseList);
3951
- append(incomingList);
3952
- return merged;
3953
- },
3954
-
3955
- extractPathOptionsFromSessions(sessions) {
3956
- const paths = [];
3957
- if (!Array.isArray(sessions)) {
3958
- return paths;
3959
- }
3960
-
3961
- const seen = new Set();
3962
- for (const session of sessions) {
3963
- const value = this.normalizeSessionPathValue(session && session.cwd ? session.cwd : '');
3964
- if (!value) continue;
3965
- const key = value.toLowerCase();
3966
- if (seen.has(key)) continue;
3967
- seen.add(key);
3968
- paths.push(value);
3969
- }
3970
- return paths;
3971
- },
3972
-
3973
- syncSessionPathOptionsForSource(source, nextOptions, mergeWithExisting = false) {
3974
- const targetSource = source === 'codex' || source === 'claude' ? source : 'all';
3975
- const current = Array.isArray(this.sessionPathOptionsMap[targetSource])
3976
- ? this.sessionPathOptionsMap[targetSource]
3977
- : [];
3978
- const merged = mergeWithExisting
3979
- ? this.mergeSessionPathOptions(current, nextOptions)
3980
- : this.mergeSessionPathOptions([], nextOptions);
3981
- this.sessionPathOptionsMap = {
3982
- ...this.sessionPathOptionsMap,
3983
- [targetSource]: merged
3984
- };
3985
- this.refreshSessionPathOptions(targetSource);
3986
- },
3987
-
3988
- refreshSessionPathOptions(source) {
3989
- const targetSource = source === 'codex' || source === 'claude' ? source : 'all';
3990
- const base = Array.isArray(this.sessionPathOptionsMap[targetSource])
3991
- ? [...this.sessionPathOptionsMap[targetSource]]
3992
- : [];
3993
- const selected = this.normalizeSessionPathValue(this.sessionPathFilter);
3994
- if (selected && !base.some(item => item.toLowerCase() === selected.toLowerCase())) {
3995
- base.unshift(selected);
3996
- }
3997
- if (targetSource === this.sessionFilterSource) {
3998
- this.sessionPathOptions = base;
3999
- }
4000
- },
4001
-
4002
- async loadSessionPathOptions(options = {}) {
4003
- const source = options.source === 'codex' || options.source === 'claude'
4004
- ? options.source
4005
- : 'all';
4006
- const forceRefresh = !!options.forceRefresh;
4007
- const loaded = !!this.sessionPathOptionsLoadedMap[source];
4008
- if (!forceRefresh && loaded) {
4009
- return;
4010
- }
4011
-
4012
- const requestSeq = ++this.sessionPathRequestSeq;
4013
- this.sessionPathOptionsLoading = true;
4014
- try {
4015
- const res = await api('list-session-paths', {
4016
- source,
4017
- limit: 500,
4018
- forceRefresh
4019
- });
4020
- if (requestSeq !== this.sessionPathRequestSeq) {
4021
- return;
4022
- }
4023
- if (res && !res.error && Array.isArray(res.paths)) {
4024
- this.syncSessionPathOptionsForSource(source, res.paths, true);
4025
- this.sessionPathOptionsLoadedMap = {
4026
- ...this.sessionPathOptionsLoadedMap,
4027
- [source]: true
4028
- };
4029
- }
4030
- } catch (_) {
4031
- // 路径补全失败不影响会话主流程
4032
- } finally {
4033
- if (requestSeq === this.sessionPathRequestSeq) {
4034
- this.sessionPathOptionsLoading = false;
4035
- }
4036
- }
4037
- },
4038
-
4039
- async onSessionSourceChange() {
4040
- this.refreshSessionPathOptions(this.sessionFilterSource);
4041
- await this.loadSessions();
4042
- },
4043
-
4044
- async onSessionPathFilterChange() {
4045
- await this.loadSessions();
4046
- },
4047
-
4048
- async onSessionFilterChange() {
4049
- await this.loadSessions();
4050
- },
4051
-
4052
- async clearSessionFilters() {
4053
- this.sessionFilterSource = 'all';
4054
- this.sessionPathFilter = '';
4055
- this.sessionQuery = '';
4056
- this.sessionRoleFilter = 'all';
4057
- this.sessionTimePreset = 'all';
4058
- await this.onSessionSourceChange();
4059
- },
4060
-
4061
- getRecordKey(message) {
4062
- if (!message || !Number.isInteger(message.recordLineIndex) || message.recordLineIndex < 0) {
4063
- return '';
4064
- }
4065
- return String(message.recordLineIndex);
4066
- },
4067
-
4068
- getRecordRenderKey(message, idx) {
4069
- const recordKey = this.getRecordKey(message);
4070
- if (recordKey) {
4071
- return `record-${recordKey}`;
4072
- }
4073
- return `record-fallback-${idx}-${message && message.timestamp ? message.timestamp : ''}`;
4074
- },
4075
-
4076
- syncActiveSessionMessageCount(messageCount) {
4077
- if (!Number.isFinite(messageCount) || messageCount < 0) return;
4078
- if (this.activeSession) {
4079
- this.activeSession.messageCount = messageCount;
4080
- }
4081
- const activeKey = this.activeSession ? this.getSessionExportKey(this.activeSession) : '';
4082
- if (!activeKey) return;
4083
- const matched = this.sessionsList.find(item => this.getSessionExportKey(item) === activeKey);
4084
- if (matched) {
4085
- matched.messageCount = messageCount;
4086
- }
4087
- },
4088
-
4089
- async loadSessions() {
4090
- if (this.sessionsLoading) return;
4091
- this.sessionsLoading = true;
4092
- this.activeSessionDetailError = '';
4093
- const query = this.isSessionQueryEnabled ? this.sessionQuery : '';
4094
- try {
4095
- const res = await api('list-sessions', {
4096
- source: this.sessionFilterSource,
4097
- pathFilter: this.sessionPathFilter,
4098
- query,
4099
- queryMode: 'and',
4100
- queryScope: 'content',
4101
- contentScanLimit: 50,
4102
- roleFilter: this.sessionRoleFilter,
4103
- timeRangePreset: this.sessionTimePreset,
4104
- limit: 200,
4105
- forceRefresh: true
4106
- });
4107
- if (res.error) {
4108
- this.showMessage(res.error, 'error');
4109
- this.sessionsList = [];
4110
- this.activeSession = null;
4111
- this.activeSessionMessages = [];
4112
- this.activeSessionDetailClipped = false;
4113
- } else {
4114
- this.sessionsList = Array.isArray(res.sessions) ? res.sessions : [];
4115
- this.syncSessionPathOptionsForSource(
4116
- this.sessionFilterSource,
4117
- this.extractPathOptionsFromSessions(this.sessionsList),
4118
- true
4119
- );
4120
- if (this.sessionsList.length === 0) {
4121
- this.activeSession = null;
4122
- this.activeSessionMessages = [];
4123
- this.activeSessionDetailClipped = false;
4124
- } else {
4125
- const oldKey = this.activeSession ? this.getSessionExportKey(this.activeSession) : '';
4126
- const matched = this.sessionsList.find(item => this.getSessionExportKey(item) === oldKey);
4127
- this.activeSession = matched || this.sessionsList[0];
4128
- await this.loadActiveSessionDetail();
4129
- }
4130
- void this.loadSessionPathOptions({ source: this.sessionFilterSource });
4131
- }
4132
- } catch (e) {
4133
- this.sessionsList = [];
4134
- this.activeSession = null;
4135
- this.activeSessionMessages = [];
4136
- this.activeSessionDetailClipped = false;
4137
- this.showMessage('加载会话失败: ' + e.message, 'error');
4138
- } finally {
4139
- this.sessionsLoading = false;
4140
- }
4141
- },
4142
-
4143
- async selectSession(session) {
4144
- if (!session) return;
4145
- if (this.activeSession && this.getSessionExportKey(this.activeSession) === this.getSessionExportKey(session)) return;
4146
- this.activeSession = session;
4147
- this.activeSessionMessages = [];
4148
- this.activeSessionDetailError = '';
4149
- this.activeSessionDetailClipped = false;
4150
- await this.loadActiveSessionDetail();
4151
- },
4152
-
4153
- async loadSessionStandalonePlain() {
4154
- if (!this.activeSession) {
4155
- this.sessionStandaloneText = '';
4156
- this.sessionStandaloneTitle = '会话';
4157
- this.sessionStandaloneSourceLabel = '';
4158
- this.sessionStandaloneError = '';
4159
- return;
4160
- }
4161
-
4162
- const requestSeq = ++this.sessionStandaloneRequestSeq;
4163
- this.sessionStandaloneLoading = true;
4164
- this.sessionStandaloneError = '';
4165
- try {
4166
- const res = await api('session-plain', {
4167
- source: this.activeSession.source,
4168
- sessionId: this.activeSession.sessionId,
4169
- filePath: this.activeSession.filePath
4170
- });
4171
-
4172
- if (requestSeq !== this.sessionStandaloneRequestSeq) {
4173
- return;
4174
- }
4175
-
4176
- if (res.error) {
4177
- this.sessionStandaloneText = '';
4178
- this.sessionStandaloneError = res.error;
4179
- return;
4180
- }
4181
-
4182
- this.sessionStandaloneSourceLabel = res.sourceLabel || this.activeSession.sourceLabel || '';
4183
- this.sessionStandaloneTitle = res.sessionId || this.activeSession.title || '会话';
4184
- this.sessionStandaloneText = typeof res.text === 'string' ? res.text : '';
4185
- } catch (e) {
4186
- if (requestSeq !== this.sessionStandaloneRequestSeq) {
4187
- return;
4188
- }
4189
- this.sessionStandaloneText = '';
4190
- this.sessionStandaloneError = '加载会话内容失败: ' + e.message;
4191
- } finally {
4192
- if (requestSeq === this.sessionStandaloneRequestSeq) {
4193
- this.sessionStandaloneLoading = false;
4194
- }
4195
- }
4196
- },
4197
-
4198
- async loadActiveSessionDetail() {
4199
- if (!this.activeSession) {
4200
- this.activeSessionMessages = [];
4201
- this.activeSessionDetailError = '';
4202
- this.activeSessionDetailClipped = false;
4203
- return;
4204
- }
4205
-
4206
- const requestSeq = ++this.sessionDetailRequestSeq;
4207
- this.sessionDetailLoading = true;
4208
- this.activeSessionDetailError = '';
4209
- try {
4210
- const res = await api('session-detail', {
4211
- source: this.activeSession.source,
4212
- sessionId: this.activeSession.sessionId,
4213
- filePath: this.activeSession.filePath,
4214
- messageLimit: 300
4215
- });
4216
-
4217
- if (requestSeq !== this.sessionDetailRequestSeq) {
4218
- return;
4219
- }
4220
-
4221
- if (res.error) {
4222
- this.activeSessionMessages = [];
4223
- this.activeSessionDetailClipped = false;
4224
- this.activeSessionDetailError = res.error;
4225
- return;
4226
- }
4227
-
4228
- this.activeSessionMessages = Array.isArray(res.messages) ? res.messages : [];
4229
- this.activeSessionDetailClipped = !!res.clipped;
4230
- if (this.activeSession) {
4231
- if (res.sourceLabel) {
4232
- this.activeSession.sourceLabel = res.sourceLabel;
4233
- }
4234
- if (res.sessionId) {
4235
- this.activeSession.sessionId = res.sessionId;
4236
- if (!this.activeSession.title) {
4237
- this.activeSession.title = res.sessionId;
4238
- }
4239
- }
4240
- if (res.filePath) {
4241
- this.activeSession.filePath = res.filePath;
4242
- }
4243
- }
4244
- if (res.updatedAt) {
4245
- this.activeSession.updatedAt = res.updatedAt;
4246
- }
4247
- if (res.cwd) {
4248
- this.activeSession.cwd = res.cwd;
4249
- }
4250
- if (Number.isFinite(res.totalMessages)) {
4251
- this.syncActiveSessionMessageCount(res.totalMessages);
4252
- }
4253
- } catch (e) {
4254
- if (requestSeq !== this.sessionDetailRequestSeq) {
4255
- return;
4256
- }
4257
- this.activeSessionMessages = [];
4258
- this.activeSessionDetailClipped = false;
4259
- this.activeSessionDetailError = '加载会话内容失败: ' + e.message;
4260
- } finally {
4261
- if (requestSeq === this.sessionDetailRequestSeq) {
4262
- this.sessionDetailLoading = false;
4263
- }
4264
- }
4265
- },
4266
-
4267
- downloadTextFile(fileName, content) {
4268
- const blob = new Blob([content], { type: 'text/markdown;charset=utf-8' });
4269
- const url = URL.createObjectURL(blob);
4270
- const link = document.createElement('a');
4271
- link.href = url;
4272
- link.download = fileName;
4273
- link.click();
4274
- URL.revokeObjectURL(url);
4275
- },
4276
-
4277
- async exportSession(session) {
4278
- const key = this.getSessionExportKey(session);
4279
- if (this.sessionExporting[key]) return;
4280
-
4281
- this.sessionExporting[key] = true;
4282
- try {
4283
- const res = await api('export-session', {
4284
- source: session.source,
4285
- sessionId: session.sessionId,
4286
- filePath: session.filePath
4287
- });
4288
- if (res.error) {
4289
- this.showMessage(res.error, 'error');
4290
- return;
4291
- }
4292
-
4293
- const fileName = res.fileName || `${session.source || 'session'}-${session.sessionId || Date.now()}.md`;
4294
- this.downloadTextFile(fileName, res.content || '');
4295
- if (res.truncated) {
4296
- const maxLabel = res.maxMessages === 'all' ? 'all' : res.maxMessages;
4297
- this.showMessage(`会话导出完成(已截断:最多 ${maxLabel} 条消息)`, 'info');
4298
- } else {
4299
- this.showMessage('会话导出完成', 'success');
4300
- }
4301
- } catch (e) {
4302
- this.showMessage('导出失败: ' + e.message, 'error');
4303
- } finally {
4304
- this.sessionExporting[key] = false;
4305
- }
4306
- },
4307
-
4308
- async switchProvider(name) {
4309
- this.currentProvider = name;
4310
- await this.loadModelsForProvider(name);
4311
- await this.openConfigTemplateEditor();
4312
- },
4313
-
4314
- async onModelChange() {
4315
- await this.openConfigTemplateEditor();
4316
- },
4317
-
4318
- async loadRecentConfigs() {
4319
- this.recentLoading = true;
4320
- try {
4321
- const res = await api('get-recent-configs');
4322
- if (res && Array.isArray(res.items)) {
4323
- this.recentConfigs = res.items;
4324
- } else {
4325
- this.recentConfigs = [];
4326
- }
4327
- } catch (e) {
4328
- this.recentConfigs = [];
4329
- } finally {
4330
- this.recentLoading = false;
4331
- }
4332
- },
4333
-
4334
- async applyRecentConfig(item) {
4335
- if (!item || !item.provider || !item.model) {
4336
- this.showMessage('最近配置无效,无法应用', 'error');
4337
- return;
4338
- }
4339
- this.currentProvider = item.provider;
4340
- this.currentModel = item.model;
4341
- await this.openConfigTemplateEditor({
4342
- appendHint: '最近使用配置,确认后将写入 config.toml'
4343
- });
4344
- },
4345
-
4346
- async runHealthCheck() {
4347
- this.healthCheckLoading = true;
4348
- this.healthCheckResult = null;
4349
- try {
4350
- const res = await api('config-health-check', {
4351
- remote: false
4352
- });
4353
- if (res && typeof res === 'object') {
4354
- const issues = Array.isArray(res.issues) ? [...res.issues] : [];
4355
- let remote = res.remote || null;
4356
- {
4357
- const providers = (this.providersList || [])
4358
- .filter(provider => provider && provider.name);
4359
- const tasks = providers.map(provider =>
4360
- this.runSpeedTest(provider.name, { silent: true })
4361
- .then(result => ({ name: provider.name, result }))
4362
- .catch(err => ({
4363
- name: provider.name,
4364
- result: { ok: false, error: err && err.message ? err.message : 'Speed test failed' }
4365
- }))
4366
- );
4367
- const pairs = await Promise.all(tasks);
4368
- const results = {};
4369
- for (const pair of pairs) {
4370
- results[pair.name] = pair.result || null;
4371
- const issue = this.buildSpeedTestIssue(pair.name, pair.result);
4372
- if (issue) issues.push(issue);
4373
- }
4374
- remote = {
4375
- type: 'speed-test',
4376
- results
4377
- };
4378
- }
4379
-
4380
- const ok = issues.length === 0;
4381
- this.healthCheckResult = {
4382
- ...res,
4383
- ok,
4384
- issues,
4385
- remote
4386
- };
4387
- if (ok) {
4388
- this.showMessage('健康检查通过', 'success');
4389
- }
4390
- } else {
4391
- this.healthCheckResult = null;
4392
- this.showMessage('健康检查失败:返回数据异常', 'error');
4393
- }
4394
- } catch (e) {
4395
- this.healthCheckResult = null;
4396
- this.showMessage('健康检查失败: ' + e.message, 'error');
4397
- } finally {
4398
- this.healthCheckLoading = false;
4399
- }
4400
- },
4401
-
4402
- escapeTomlString(value) {
4403
- return String(value || '')
4404
- .replace(/\\/g, '\\\\')
4405
- .replace(/"/g, '\\"');
4406
- },
4407
-
4408
- async openConfigTemplateEditor(options = {}) {
4409
- try {
4410
- const res = await api('get-config-template', {
4411
- provider: this.currentProvider,
4412
- model: this.currentModel
4413
- });
4414
- if (res.error) {
4415
- this.showMessage(res.error, 'error');
4416
- return;
4417
- }
4418
- let template = res.template || '';
4419
- const appendHint = typeof options.appendHint === 'string' ? options.appendHint.trim() : '';
4420
- const appendBlock = typeof options.appendBlock === 'string' ? options.appendBlock.trim() : '';
4421
- if (appendHint) {
4422
- template = `${template.trimEnd()}\n\n# -------------------------------\n# ${appendHint}\n# -------------------------------\n`;
4423
- }
4424
- if (appendBlock) {
4425
- template = `${template.trimEnd()}\n\n${appendBlock}\n`;
4426
- }
4427
- this.configTemplateContent = template;
4428
- this.showConfigTemplateModal = true;
4429
- } catch (e) {
4430
- this.showMessage('加载模板失败: ' + e.message, 'error');
4431
- }
4432
- },
4433
-
4434
- closeConfigTemplateModal() {
4435
- this.showConfigTemplateModal = false;
4436
- this.configTemplateContent = '';
4437
- },
4438
-
4439
- async applyConfigTemplate() {
4440
- if (!this.configTemplateContent || !this.configTemplateContent.trim()) {
4441
- this.showMessage('模板内容不能为空', 'error');
4442
- return;
4443
- }
4444
-
4445
- this.configTemplateApplying = true;
4446
- try {
4447
- const res = await api('apply-config-template', {
4448
- template: this.configTemplateContent
4449
- });
4450
- if (res.error) {
4451
- this.showMessage(res.error, 'error');
4452
- return;
4453
- }
4454
- this.showMessage('模板已应用到 config.toml', 'success');
4455
- this.closeConfigTemplateModal();
4456
- await this.loadAll();
4457
- } catch (e) {
4458
- this.showMessage('应用模板失败: ' + e.message, 'error');
4459
- } finally {
4460
- this.configTemplateApplying = false;
4461
- }
4462
- },
4463
-
4464
- async openAgentsEditor() {
4465
- this.setAgentsModalContext('codex');
4466
- this.agentsLoading = true;
4467
- try {
4468
- const res = await api('get-agents-file');
4469
- if (res.error) {
4470
- this.showMessage(res.error, 'error');
4471
- return;
4472
- }
4473
- this.agentsContent = res.content || '';
4474
- this.agentsPath = res.path || '';
4475
- this.agentsExists = !!res.exists;
4476
- this.agentsLineEnding = res.lineEnding === '\r\n' ? '\r\n' : '\n';
4477
- this.showAgentsModal = true;
4478
- } catch (e) {
4479
- this.showMessage('加载 AGENTS.md 失败: ' + e.message, 'error');
4480
- } finally {
4481
- this.agentsLoading = false;
4482
- }
4483
- },
4484
-
4485
- async openOpenclawAgentsEditor() {
4486
- this.setAgentsModalContext('openclaw');
4487
- this.agentsLoading = true;
4488
- try {
4489
- const res = await api('get-openclaw-agents-file');
4490
- if (res.error) {
4491
- this.showMessage(res.error, 'error');
4492
- return;
4493
- }
4494
- if (res.configError) {
4495
- this.showMessage(`OpenClaw 配置解析失败,已使用默认 Workspace:${res.configError}`, 'error');
4496
- }
4497
- this.agentsContent = res.content || '';
4498
- this.agentsPath = res.path || '';
4499
- this.agentsExists = !!res.exists;
4500
- this.agentsLineEnding = res.lineEnding === '\r\n' ? '\r\n' : '\n';
4501
- this.showAgentsModal = true;
4502
- } catch (e) {
4503
- this.showMessage('加载 OpenClaw AGENTS.md 失败: ' + e.message, 'error');
4504
- } finally {
4505
- this.agentsLoading = false;
4506
- }
4507
- },
4508
-
4509
- async openOpenclawWorkspaceEditor() {
4510
- const fileName = (this.openclawWorkspaceFileName || '').trim();
4511
- if (!fileName) {
4512
- this.showMessage('请输入工作区文件名', 'error');
4513
- return;
4514
- }
4515
- this.setAgentsModalContext('openclaw-workspace', { fileName });
4516
- this.agentsLoading = true;
4517
- try {
4518
- const res = await api('get-openclaw-workspace-file', { fileName });
4519
- if (res.error) {
4520
- this.showMessage(res.error, 'error');
4521
- return;
4522
- }
4523
- if (res.configError) {
4524
- this.showMessage(`OpenClaw 配置解析失败,已使用默认 Workspace:${res.configError}`, 'error');
4525
- }
4526
- this.agentsContent = res.content || '';
4527
- this.agentsPath = res.path || '';
4528
- this.agentsExists = !!res.exists;
4529
- this.agentsLineEnding = res.lineEnding === '\r\n' ? '\r\n' : '\n';
4530
- this.showAgentsModal = true;
4531
- } catch (e) {
4532
- this.showMessage('加载 OpenClaw 工作区文件失败: ' + e.message, 'error');
4533
- } finally {
4534
- this.agentsLoading = false;
4535
- }
4536
- },
4537
-
4538
- setAgentsModalContext(context, options = {}) {
4539
- if (context === 'openclaw-workspace') {
4540
- const fileName = (options.fileName || this.openclawWorkspaceFileName || 'AGENTS.md').trim();
4541
- this.agentsContext = 'openclaw-workspace';
4542
- this.agentsWorkspaceFileName = fileName;
4543
- this.agentsModalTitle = `OpenClaw 工作区文件: ${fileName}`;
4544
- this.agentsModalHint = `保存后会写入 OpenClaw Workspace 下的 ${fileName}。`;
4545
- return;
4546
- }
4547
- this.agentsContext = context === 'openclaw' ? 'openclaw' : 'codex';
4548
- if (this.agentsContext === 'openclaw') {
4549
- this.agentsModalTitle = 'OpenClaw AGENTS.md 编辑器';
4550
- this.agentsModalHint = '保存后会写入 OpenClaw Workspace 下的 AGENTS.md。';
4551
- } else {
4552
- this.agentsModalTitle = 'AGENTS.md 编辑器';
4553
- this.agentsModalHint = '保存后会写入目标 AGENTS.md(与 config.toml 同级)。';
4554
- }
4555
- this.agentsWorkspaceFileName = '';
4556
- },
4557
-
4558
- closeAgentsModal() {
4559
- this.showAgentsModal = false;
4560
- this.agentsContent = '';
4561
- this.agentsPath = '';
4562
- this.agentsExists = false;
4563
- this.agentsLineEnding = '\n';
4564
- this.agentsSaving = false;
4565
- this.agentsWorkspaceFileName = '';
4566
- this.setAgentsModalContext('codex');
4567
- },
4568
-
4569
- async applyAgentsContent() {
4570
- this.agentsSaving = true;
4571
- try {
4572
- let action = 'apply-agents-file';
4573
- const params = {
4574
- content: this.agentsContent,
4575
- lineEnding: this.agentsLineEnding
4576
- };
4577
- if (this.agentsContext === 'openclaw') {
4578
- action = 'apply-openclaw-agents-file';
4579
- } else if (this.agentsContext === 'openclaw-workspace') {
4580
- action = 'apply-openclaw-workspace-file';
4581
- params.fileName = this.agentsWorkspaceFileName;
4582
- }
4583
- const res = await api(action, params);
4584
- if (res.error) {
4585
- this.showMessage(res.error, 'error');
4586
- return;
4587
- }
4588
- const successLabel = this.agentsContext === 'openclaw-workspace'
4589
- ? `工作区文件已保存${this.agentsWorkspaceFileName ? `: ${this.agentsWorkspaceFileName}` : ''}`
4590
- : (this.agentsContext === 'openclaw' ? 'OpenClaw AGENTS.md 已保存' : 'AGENTS.md 已保存');
4591
- this.showMessage(successLabel, 'success');
4592
- this.closeAgentsModal();
4593
- } catch (e) {
4594
- this.showMessage('保存文件失败: ' + e.message, 'error');
4595
- } finally {
4596
- this.agentsSaving = false;
4597
- }
4598
- },
4599
-
4600
- async addProvider() {
4601
- if (!this.newProvider.name || !this.newProvider.url) {
4602
- return this.showMessage('名称和URL必填', 'error');
4603
- }
4604
- const name = this.newProvider.name.trim();
4605
- if (!name) {
4606
- return this.showMessage('名称不能为空', 'error');
4607
- }
4608
- if (this.providersList.some(item => item.name === name)) {
4609
- return this.showMessage('提供商已存在', 'error');
4610
- }
4611
-
4612
- const safeName = this.escapeTomlString(name);
4613
- const safeUrl = this.escapeTomlString(this.newProvider.url.trim());
4614
- const safeKey = this.escapeTomlString(this.newProvider.key || '');
4615
- 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`;
4616
-
4617
- this.currentProvider = name;
4618
- this.showMessage('已生成新增模板,请确认后应用', 'info');
4619
- this.closeAddModal();
4620
- await this.openConfigTemplateEditor({
4621
- appendHint: `新增 provider: ${name}(请检查字段后应用)`,
4622
- appendBlock: newProviderBlock
4623
- });
4624
- },
4625
-
4626
- async deleteProvider(name) {
4627
- if (!confirm(`确定删除提供商 "${name}"?`)) return;
4628
- this.showMessage('请在模板中手动删除该 provider 配置块后应用', 'info');
4629
- await this.openConfigTemplateEditor({
4630
- appendHint: `请手动删除 [model_providers.${name}] 配置块,并确认 model_provider 指向有效 provider`
4631
- });
4632
- },
4633
-
4634
- openEditModal(provider) {
4635
- this.editingProvider = {
4636
- name: provider.name,
4637
- url: provider.url || '',
4638
- key: ''
4639
- };
4640
- this.showEditModal = true;
4641
- },
4642
-
4643
- async updateProvider() {
4644
- if (!this.editingProvider.url) {
4645
- return this.showMessage('URL 必填', 'error');
4646
- }
4647
-
4648
- const name = this.editingProvider.name;
4649
- const safeUrl = this.escapeTomlString(this.editingProvider.url.trim());
4650
- const safeKey = this.escapeTomlString(this.editingProvider.key || '');
4651
- this.closeEditModal();
4652
- this.showMessage('已生成更新模板,请确认后应用', 'info');
4653
- await this.openConfigTemplateEditor({
4654
- appendHint: `请将 [model_providers.${name}] 中 base_url 更新为 ${safeUrl}${safeKey ? ',并更新 preferred_auth_method' : ''}`
4655
- });
4656
- },
4657
-
4658
- closeEditModal() {
4659
- this.showEditModal = false;
4660
- this.editingProvider = { name: '', url: '', key: '' };
4661
- },
4662
-
4663
- async addModel() {
4664
- if (!this.newModelName || !this.newModelName.trim()) {
4665
- return this.showMessage('请输入模型名称', 'error');
4666
- }
4667
- const res = await api('add-model', { model: this.newModelName.trim() });
4668
- if (res.error) {
4669
- this.showMessage(res.error, 'error');
4670
- } else {
4671
- this.showMessage('已添加', 'success');
4672
- this.closeModelModal();
4673
- await this.loadAll();
4674
- }
4675
- },
4676
-
4677
- async removeModel(model) {
4678
- if (!confirm(`确定删除模型 "${model}"?`)) return;
4679
- const res = await api('delete-model', { model });
4680
- if (res.error) {
4681
- this.showMessage(res.error, 'error');
4682
- } else {
4683
- this.showMessage('已删除', 'success');
4684
- await this.loadAll();
4685
- }
4686
- },
4687
-
4688
- closeAddModal() {
4689
- this.showAddModal = false;
4690
- this.newProvider = { name: '', url: '', key: '' };
4691
- },
4692
-
4693
- closeModelModal() {
4694
- this.showModelModal = false;
4695
- this.newModelName = '';
4696
- },
4697
-
4698
- formatKey(key) {
4699
- if (!key) return '(未设置)';
4700
- if (key.length > 10) {
4701
- return key.substring(0, 3) + '****' + key.substring(key.length - 3);
4702
- }
4703
- return '****';
4704
- },
4705
-
4706
- displayApiKey(configName) {
4707
- const key = this.claudeConfigs[configName]?.apiKey;
4708
- return this.formatKey(key);
4709
- },
4710
-
4711
- switchClaudeConfig(name) {
4712
- this.currentClaudeConfig = name;
4713
- this.refreshClaudeModelContext();
4714
- },
4715
-
4716
- onClaudeModelChange() {
4717
- const name = this.currentClaudeConfig;
4718
- if (!name) {
4719
- this.showMessage('请先选择配置', 'error');
4720
- return;
4721
- }
4722
- const model = (this.currentClaudeModel || '').trim();
4723
- if (!model) {
4724
- this.showMessage('请输入模型', 'error');
4725
- return;
4726
- }
4727
- const existing = this.claudeConfigs[name] || {};
4728
- this.currentClaudeModel = model;
4729
- this.claudeConfigs[name] = {
4730
- apiKey: existing.apiKey || '',
4731
- baseUrl: existing.baseUrl || '',
4732
- model: model,
4733
- hasKey: !!existing.apiKey
4734
- };
4735
- this.saveClaudeConfigs();
4736
- this.updateClaudeModelsCurrent();
4737
- if (!this.claudeConfigs[name].apiKey) {
4738
- this.showMessage('该配置未设置 API Key,请先编辑', 'error');
4739
- return;
4740
- }
4741
- this.applyClaudeConfig(name);
4742
- },
4743
-
4744
- saveClaudeConfigs() {
4745
- localStorage.setItem('claudeConfigs', JSON.stringify(this.claudeConfigs));
4746
- },
4747
-
4748
- openEditConfigModal(name) {
4749
- const config = this.claudeConfigs[name];
4750
- this.editingConfig = {
4751
- name: name,
4752
- apiKey: config.apiKey || '',
4753
- baseUrl: config.baseUrl || '',
4754
- model: config.model || ''
4755
- };
4756
- this.showEditConfigModal = true;
4757
- },
4758
-
4759
- updateConfig() {
4760
- const name = this.editingConfig.name;
4761
- this.claudeConfigs[name] = {
4762
- apiKey: this.editingConfig.apiKey,
4763
- baseUrl: this.editingConfig.baseUrl,
4764
- model: this.editingConfig.model,
4765
- hasKey: !!this.editingConfig.apiKey
4766
- };
4767
- this.saveClaudeConfigs();
4768
- this.showMessage('配置已更新', 'success');
4769
- this.closeEditConfigModal();
4770
- if (name === this.currentClaudeConfig) {
4771
- this.refreshClaudeModelContext();
4772
- }
4773
- },
4774
-
4775
- closeEditConfigModal() {
4776
- this.showEditConfigModal = false;
4777
- this.editingConfig = { name: '', apiKey: '', baseUrl: '', model: '' };
4778
- },
4779
-
4780
- async saveAndApplyConfig() {
4781
- const name = this.editingConfig.name;
4782
- this.claudeConfigs[name] = {
4783
- apiKey: this.editingConfig.apiKey,
4784
- baseUrl: this.editingConfig.baseUrl,
4785
- model: this.editingConfig.model,
4786
- hasKey: !!this.editingConfig.apiKey
4787
- };
4788
- this.saveClaudeConfigs();
4789
-
4790
- const config = this.claudeConfigs[name];
4791
- if (!config.apiKey) {
4792
- this.showMessage('已保存,未应用:请先输入 API Key', 'info');
4793
- this.closeEditConfigModal();
4794
- if (name === this.currentClaudeConfig) {
4795
- this.refreshClaudeModelContext();
4796
- }
4797
- return;
4798
- }
4799
-
4800
- const res = await api('apply-claude-config', { config });
4801
- if (res.error || res.success === false) {
4802
- this.showMessage(res.error || '应用 Claude 配置失败', 'error');
4803
- } else {
4804
- const targetTip = res.targetPath ? `(${res.targetPath})` : '';
4805
- this.showMessage(`已保存并应用到 Claude 配置${targetTip}`, 'success');
4806
- this.closeEditConfigModal();
4807
- if (name === this.currentClaudeConfig) {
4808
- this.refreshClaudeModelContext();
4809
- }
4810
- }
4811
- },
4812
-
4813
- addClaudeConfig() {
4814
- if (!this.newClaudeConfig.name || !this.newClaudeConfig.name.trim()) {
4815
- return this.showMessage('请输入配置名称', 'error');
4816
- }
4817
- const name = this.newClaudeConfig.name.trim();
4818
- if (this.claudeConfigs[name]) {
4819
- return this.showMessage('配置名称已存在', 'error');
4820
- }
4821
-
4822
- this.claudeConfigs[name] = {
4823
- apiKey: this.newClaudeConfig.apiKey,
4824
- baseUrl: this.newClaudeConfig.baseUrl,
4825
- model: this.newClaudeConfig.model,
4826
- hasKey: !!this.newClaudeConfig.apiKey
4827
- };
4828
-
4829
- this.currentClaudeConfig = name;
4830
- this.saveClaudeConfigs();
4831
- this.showMessage('配置已添加', 'success');
4832
- this.closeClaudeConfigModal();
4833
- this.refreshClaudeModelContext();
4834
- },
4835
-
4836
- deleteClaudeConfig(name) {
4837
- if (Object.keys(this.claudeConfigs).length <= 1) {
4838
- return this.showMessage('至少保留一个配置', 'error');
4839
- }
4840
-
4841
- if (!confirm(`确定删除配置 "${name}"?`)) return;
4842
-
4843
- delete this.claudeConfigs[name];
4844
- if (this.currentClaudeConfig === name) {
4845
- this.currentClaudeConfig = Object.keys(this.claudeConfigs)[0];
4846
- }
4847
- this.saveClaudeConfigs();
4848
- this.showMessage('配置已删除', 'success');
4849
- this.refreshClaudeModelContext();
4850
- },
4851
-
4852
- async applyClaudeConfig(name) {
4853
- this.currentClaudeConfig = name;
4854
- this.refreshClaudeModelContext();
4855
- const config = this.claudeConfigs[name];
4856
-
4857
- if (!config.apiKey) {
4858
- return this.showMessage('该配置未设置 API Key,请先编辑', 'error');
4859
- }
4860
-
4861
- const res = await api('apply-claude-config', { config });
4862
- if (res.error || res.success === false) {
4863
- this.showMessage(res.error || '应用 Claude 配置失败', 'error');
4864
- } else {
4865
- const targetTip = res.targetPath ? `(${res.targetPath})` : '';
4866
- this.showMessage(`已应用配置到 Claude 设置: ${name}${targetTip}`, 'success');
4867
- }
4868
- },
4869
-
4870
- closeClaudeConfigModal() {
4871
- this.showClaudeConfigModal = false;
4872
- this.newClaudeConfig = {
4873
- name: '',
4874
- apiKey: '',
4875
- baseUrl: 'https://open.bigmodel.cn/api/anthropic',
4876
- model: 'glm-4.7'
4877
- };
4878
- },
4879
-
4880
- getOpenclawParser() {
4881
- if (window.JSON5 && typeof window.JSON5.parse === 'function' && typeof window.JSON5.stringify === 'function') {
4882
- return {
4883
- parse: window.JSON5.parse,
4884
- stringify: window.JSON5.stringify
4885
- };
4886
- }
4887
- return {
4888
- parse: JSON.parse,
4889
- stringify: JSON.stringify
4890
- };
4891
- },
4892
-
4893
- parseOpenclawContent(content, options = {}) {
4894
- const allowEmpty = !!options.allowEmpty;
4895
- const raw = typeof content === 'string' ? content.trim() : '';
4896
- if (!raw) {
4897
- if (allowEmpty) {
4898
- return { ok: true, data: {} };
4899
- }
4900
- return { ok: false, error: '配置内容为空' };
4901
- }
4902
- try {
4903
- const parser = this.getOpenclawParser();
4904
- const data = parser.parse(raw);
4905
- if (!data || typeof data !== 'object' || Array.isArray(data)) {
4906
- return { ok: false, error: '配置格式错误(根节点必须是对象)' };
4907
- }
4908
- return { ok: true, data };
4909
- } catch (e) {
4910
- return { ok: false, error: e.message || '解析失败' };
4911
- }
4912
- },
4913
-
4914
- stringifyOpenclawConfig(data) {
4915
- const parser = this.getOpenclawParser();
4916
- try {
4917
- return parser.stringify(data, null, 2);
4918
- } catch (e) {
4919
- return JSON.stringify(data, null, 2);
4920
- }
4921
- },
4922
-
4923
- resetOpenclawStructured() {
4924
- this.openclawStructured = {
4925
- agentPrimary: '',
4926
- agentFallbacks: [''],
4927
- workspace: '',
4928
- timeout: '',
4929
- contextTokens: '',
4930
- maxConcurrent: '',
4931
- envItems: [{ key: '', value: '', show: false }],
4932
- toolsProfile: 'default',
4933
- toolsAllow: [''],
4934
- toolsDeny: ['']
4935
- };
4936
- this.openclawAgentsList = [];
4937
- this.openclawProviders = [];
4938
- this.openclawMissingProviders = [];
4939
- },
4940
-
4941
- getOpenclawQuickDefaults() {
4942
- return {
4943
- providerName: '',
4944
- baseUrl: '',
4945
- apiKey: '',
4946
- apiType: 'openai-responses',
4947
- modelId: '',
4948
- modelName: '',
4949
- contextWindow: '',
4950
- maxTokens: '',
4951
- setPrimary: true,
4952
- overrideProvider: true,
4953
- overrideModels: true,
4954
- showKey: false
4955
- };
4956
- },
4957
-
4958
- resetOpenclawQuick() {
4959
- this.openclawQuick = this.getOpenclawQuickDefaults();
4960
- },
4961
-
4962
- toggleOpenclawQuickKey() {
4963
- this.openclawQuick.showKey = !this.openclawQuick.showKey;
4964
- },
4965
-
4966
- fillOpenclawQuickFromConfig(config) {
4967
- const defaults = this.getOpenclawQuickDefaults();
4968
- if (!config || typeof config !== 'object' || Array.isArray(config)) {
4969
- this.openclawQuick = defaults;
4970
- return;
4971
- }
4972
-
4973
- const agentDefaults = config.agents && typeof config.agents === 'object' && !Array.isArray(config.agents)
4974
- && config.agents.defaults && typeof config.agents.defaults === 'object' && !Array.isArray(config.agents.defaults)
4975
- ? config.agents.defaults
4976
- : {};
4977
- const modelConfig = agentDefaults.model;
4978
- const legacyAgent = config.agent && typeof config.agent === 'object' && !Array.isArray(config.agent)
4979
- ? config.agent
4980
- : {};
4981
-
4982
- let primaryRef = '';
4983
- if (modelConfig && typeof modelConfig === 'object' && !Array.isArray(modelConfig) && typeof modelConfig.primary === 'string') {
4984
- primaryRef = modelConfig.primary;
4985
- } else if (typeof modelConfig === 'string') {
4986
- primaryRef = modelConfig;
4987
- }
4988
- if (!primaryRef) {
4989
- if (typeof legacyAgent.model === 'string') {
4990
- primaryRef = legacyAgent.model;
4991
- } else if (legacyAgent.model && typeof legacyAgent.model === 'object' && typeof legacyAgent.model.primary === 'string') {
4992
- primaryRef = legacyAgent.model.primary;
4993
- }
4994
- }
4995
-
4996
- let providerName = '';
4997
- let modelId = '';
4998
- if (primaryRef) {
4999
- const parts = primaryRef.split('/');
5000
- if (parts.length >= 2) {
5001
- providerName = parts.shift().trim();
5002
- modelId = parts.join('/').trim();
5003
- }
5004
- }
5005
-
5006
- const providers = config.models && typeof config.models === 'object' && !Array.isArray(config.models)
5007
- && config.models.providers && typeof config.models.providers === 'object' && !Array.isArray(config.models.providers)
5008
- ? config.models.providers
5009
- : null;
5010
- let providerConfig = providerName && providers ? providers[providerName] : null;
5011
- if (!providerName && providers) {
5012
- const providerKeys = Object.keys(providers);
5013
- if (providerKeys.length === 1) {
5014
- providerName = providerKeys[0];
5015
- providerConfig = providers[providerName];
5016
- }
5017
- }
5018
-
5019
- let modelEntry = null;
5020
- if (providerConfig && typeof providerConfig === 'object' && Array.isArray(providerConfig.models)) {
5021
- if (modelId) {
5022
- modelEntry = providerConfig.models.find(item => item && item.id === modelId);
5023
- }
5024
- if (!modelEntry && providerConfig.models.length === 1) {
5025
- modelEntry = providerConfig.models[0];
5026
- if (!modelId && modelEntry && typeof modelEntry.id === 'string') {
5027
- modelId = modelEntry.id;
5028
- }
5029
- }
5030
- }
5031
-
5032
- const baseUrl = providerConfig && typeof providerConfig === 'object' && typeof providerConfig.baseUrl === 'string'
5033
- ? providerConfig.baseUrl
5034
- : '';
5035
- const apiKey = providerConfig && typeof providerConfig === 'object' && typeof providerConfig.apiKey === 'string'
5036
- ? providerConfig.apiKey
5037
- : '';
5038
- const apiType = providerConfig && typeof providerConfig === 'object' && typeof providerConfig.api === 'string'
5039
- ? providerConfig.api
5040
- : defaults.apiType;
5041
-
5042
- this.openclawQuick = {
5043
- ...defaults,
5044
- providerName,
5045
- baseUrl,
5046
- apiKey,
5047
- apiType,
5048
- modelId: modelId || '',
5049
- modelName: modelEntry && typeof modelEntry.name === 'string' ? modelEntry.name : '',
5050
- contextWindow: modelEntry && typeof modelEntry.contextWindow === 'number'
5051
- ? String(modelEntry.contextWindow)
5052
- : '',
5053
- maxTokens: modelEntry && typeof modelEntry.maxTokens === 'number'
5054
- ? String(modelEntry.maxTokens)
5055
- : ''
5056
- };
5057
- },
5058
-
5059
- syncOpenclawQuickFromText(options = {}) {
5060
- const silent = !!options.silent;
5061
- const parsed = this.parseOpenclawContent(this.openclawEditing.content, { allowEmpty: true });
5062
- if (!parsed.ok) {
5063
- this.resetOpenclawQuick();
5064
- if (!silent) {
5065
- this.showMessage('解析 OpenClaw 配置失败: ' + parsed.error, 'error');
5066
- }
5067
- return false;
5068
- }
5069
- this.fillOpenclawQuickFromConfig(parsed.data);
5070
- if (!silent) {
5071
- this.showMessage('已从编辑器读取快速配置', 'success');
5072
- }
5073
- return true;
5074
- },
5075
-
5076
- mergeOpenclawModelEntry(existing, incoming, overwrite = false) {
5077
- if (!existing || typeof existing !== 'object' || Array.isArray(existing)) {
5078
- return { ...incoming };
5079
- }
5080
- if (overwrite) {
5081
- return { ...incoming };
5082
- }
5083
- const merged = { ...existing };
5084
- for (const [key, value] of Object.entries(incoming || {})) {
5085
- if (merged[key] === undefined || merged[key] === null || merged[key] === '') {
5086
- merged[key] = value;
5087
- }
5088
- }
5089
- return merged;
5090
- },
5091
-
5092
- fillOpenclawStructured(config) {
5093
- const defaults = config && config.agents && typeof config.agents === 'object' && !Array.isArray(config.agents)
5094
- && config.agents.defaults && typeof config.agents.defaults === 'object' && !Array.isArray(config.agents.defaults)
5095
- ? config.agents.defaults
5096
- : {};
5097
- const model = defaults.model && typeof defaults.model === 'object' && !Array.isArray(defaults.model)
5098
- ? defaults.model
5099
- : {};
5100
- const legacyAgent = config && config.agent && typeof config.agent === 'object' && !Array.isArray(config.agent)
5101
- ? config.agent
5102
- : {};
5103
- const fallbackList = Array.isArray(model.fallbacks)
5104
- ? model.fallbacks.filter(item => typeof item === 'string' && item.trim()).map(item => item.trim())
5105
- : [];
5106
- const env = config && config.env && typeof config.env === 'object' && !Array.isArray(config.env)
5107
- ? config.env
5108
- : {};
5109
- const envItems = Object.entries(env).map(([key, value]) => ({
5110
- key,
5111
- value: value == null ? '' : String(value),
5112
- show: false
5113
- }));
5114
- const tools = config && config.tools && typeof config.tools === 'object' && !Array.isArray(config.tools)
5115
- ? config.tools
5116
- : {};
5117
-
5118
- let primary = typeof model.primary === 'string' ? model.primary : '';
5119
- if (!primary) {
5120
- if (typeof legacyAgent.model === 'string') {
5121
- primary = legacyAgent.model;
5122
- } else if (legacyAgent.model && typeof legacyAgent.model === 'object' && typeof legacyAgent.model.primary === 'string') {
5123
- primary = legacyAgent.model.primary;
5124
- }
5125
- }
5126
-
5127
- this.openclawStructured = {
5128
- agentPrimary: primary,
5129
- agentFallbacks: fallbackList.length ? fallbackList : [''],
5130
- workspace: typeof defaults.workspace === 'string' ? defaults.workspace : '',
5131
- timeout: typeof defaults.timeout === 'number' && Number.isFinite(defaults.timeout)
5132
- ? String(defaults.timeout)
5133
- : '',
5134
- contextTokens: typeof defaults.contextTokens === 'number' && Number.isFinite(defaults.contextTokens)
5135
- ? String(defaults.contextTokens)
5136
- : '',
5137
- maxConcurrent: typeof defaults.maxConcurrent === 'number' && Number.isFinite(defaults.maxConcurrent)
5138
- ? String(defaults.maxConcurrent)
5139
- : '',
5140
- envItems: envItems.length ? envItems : [{ key: '', value: '', show: false }],
5141
- toolsProfile: typeof tools.profile === 'string' && tools.profile.trim() ? tools.profile : 'default',
5142
- toolsAllow: Array.isArray(tools.allow) && tools.allow.length
5143
- ? tools.allow.filter(item => typeof item === 'string' && item.trim()).map(item => item.trim())
5144
- : [''],
5145
- toolsDeny: Array.isArray(tools.deny) && tools.deny.length
5146
- ? tools.deny.filter(item => typeof item === 'string' && item.trim()).map(item => item.trim())
5147
- : ['']
5148
- };
5149
- },
5150
-
5151
- syncOpenclawStructuredFromText(options = {}) {
5152
- const silent = !!options.silent;
5153
- const parsed = this.parseOpenclawContent(this.openclawEditing.content, { allowEmpty: true });
5154
- if (!parsed.ok) {
5155
- this.resetOpenclawStructured();
5156
- this.resetOpenclawQuick();
5157
- if (!silent) {
5158
- this.showMessage('解析 OpenClaw 配置失败: ' + parsed.error, 'error');
5159
- }
5160
- return false;
5161
- }
5162
- this.fillOpenclawStructured(parsed.data);
5163
- this.fillOpenclawQuickFromConfig(parsed.data);
5164
- this.refreshOpenclawProviders(parsed.data);
5165
- this.refreshOpenclawAgentsList(parsed.data);
5166
- if (!silent) {
5167
- this.showMessage('已从文本刷新结构化配置', 'success');
5168
- }
5169
- return true;
5170
- },
5171
-
5172
- getOpenclawActiveProviders(config) {
5173
- const active = new Set();
5174
- const addProvider = (ref) => {
5175
- if (typeof ref !== 'string') return;
5176
- const text = ref.trim();
5177
- if (!text) return;
5178
- const parts = text.split('/');
5179
- if (parts.length < 2) return;
5180
- const provider = parts[0].trim();
5181
- if (provider) active.add(provider);
5182
- };
5183
- const defaults = config && config.agents && config.agents.defaults
5184
- ? config.agents.defaults
5185
- : {};
5186
- const model = defaults && defaults.model;
5187
- if (model && typeof model === 'object' && !Array.isArray(model)) {
5188
- addProvider(model.primary);
5189
- if (Array.isArray(model.fallbacks)) {
5190
- for (const item of model.fallbacks) {
5191
- addProvider(item);
5192
- }
5193
- }
5194
- } else if (typeof model === 'string') {
5195
- addProvider(model);
5196
- }
5197
- const modelsDefaults = config && config.models && config.models.defaults
5198
- ? config.models.defaults
5199
- : {};
5200
- if (modelsDefaults && typeof modelsDefaults.provider === 'string' && modelsDefaults.provider.trim()) {
5201
- active.add(modelsDefaults.provider.trim());
5202
- }
5203
- if (modelsDefaults && typeof modelsDefaults.model === 'string') {
5204
- addProvider(modelsDefaults.model);
5205
- }
5206
- return active;
5207
- },
5208
-
5209
- maskProviderValue(value) {
5210
- const text = value == null ? '' : String(value);
5211
- if (!text) return '****';
5212
- if (text.length <= 6) return '****';
5213
- return `${text.slice(0, 3)}****${text.slice(-3)}`;
5214
- },
5215
-
5216
- formatProviderValue(key, value) {
5217
- if (typeof value === 'undefined' || value === null) {
5218
- return '';
5219
- }
5220
- let text = '';
5221
- if (typeof value === 'string') {
5222
- text = value;
5223
- } else if (typeof value === 'number' || typeof value === 'boolean') {
5224
- text = String(value);
5225
- } else {
5226
- try {
5227
- text = JSON.stringify(value);
5228
- } catch (_) {
5229
- text = String(value);
5230
- }
5231
- }
5232
- if (!text) return '';
5233
- if (/key|token|secret|password/i.test(key)) {
5234
- return this.maskProviderValue(text);
5235
- }
5236
- if (text.length > 160) {
5237
- return `${text.slice(0, 157)}...`;
5238
- }
5239
- return text;
5240
- },
5241
-
5242
- collectOpenclawProviders(source, providerMap, activeProviders, entries) {
5243
- if (!providerMap || typeof providerMap !== 'object' || Array.isArray(providerMap)) {
5244
- return;
5245
- }
5246
- const keys = Object.keys(providerMap).sort();
5247
- for (const key of keys) {
5248
- const value = providerMap[key];
5249
- const fields = [];
5250
- if (value && typeof value === 'object' && !Array.isArray(value)) {
5251
- const fieldKeys = Object.keys(value).sort();
5252
- for (const fieldKey of fieldKeys) {
5253
- const fieldValue = this.formatProviderValue(fieldKey, value[fieldKey]);
5254
- if (fieldValue === '') continue;
5255
- fields.push({ key: fieldKey, value: fieldValue });
5256
- }
5257
- } else {
5258
- const fieldValue = this.formatProviderValue('value', value);
5259
- if (fieldValue !== '') {
5260
- fields.push({ key: 'value', value: fieldValue });
5261
- }
5262
- }
5263
- entries.push({
5264
- key,
5265
- source,
5266
- fields,
5267
- isActive: activeProviders.has(key)
5268
- });
5269
- }
5270
- },
5271
-
5272
- refreshOpenclawProviders(config) {
5273
- const activeProviders = this.getOpenclawActiveProviders(config || {});
5274
- const entries = [];
5275
- const modelsProviders = config && config.models ? config.models.providers : null;
5276
- const rootProviders = config && config.providers ? config.providers : null;
5277
- this.collectOpenclawProviders('models.providers', modelsProviders, activeProviders, entries);
5278
- this.collectOpenclawProviders('providers', rootProviders, activeProviders, entries);
5279
- const existing = new Set(entries.map(item => item.key));
5280
- const missing = [];
5281
- for (const provider of activeProviders) {
5282
- if (!existing.has(provider)) {
5283
- missing.push(provider);
5284
- }
5285
- }
5286
- this.openclawProviders = entries;
5287
- this.openclawMissingProviders = missing;
5288
- },
5289
-
5290
- refreshOpenclawAgentsList(config) {
5291
- const list = config && config.agents && typeof config.agents === 'object' && !Array.isArray(config.agents)
5292
- ? config.agents.list
5293
- : null;
5294
- if (!Array.isArray(list)) {
5295
- this.openclawAgentsList = [];
5296
- return;
5297
- }
5298
- const entries = [];
5299
- list.forEach((item, index) => {
5300
- if (!item || typeof item !== 'object') return;
5301
- const id = typeof item.id === 'string' && item.id.trim() ? item.id.trim() : `agent-${index + 1}`;
5302
- const identity = item.identity && typeof item.identity === 'object' && !Array.isArray(item.identity)
5303
- ? item.identity
5304
- : {};
5305
- const name = typeof identity.name === 'string' && identity.name.trim()
5306
- ? identity.name.trim()
5307
- : id;
5308
- entries.push({
5309
- key: `${id}-${index}`,
5310
- id,
5311
- name,
5312
- theme: typeof identity.theme === 'string' ? identity.theme : '',
5313
- emoji: typeof identity.emoji === 'string' ? identity.emoji : '',
5314
- avatar: typeof identity.avatar === 'string' ? identity.avatar : ''
5315
- });
5316
- });
5317
- this.openclawAgentsList = entries;
5318
- },
5319
-
5320
- normalizeStringList(list) {
5321
- if (!Array.isArray(list)) return [];
5322
- const result = [];
5323
- const seen = new Set();
5324
- for (const item of list) {
5325
- const value = typeof item === 'string' ? item.trim() : String(item || '').trim();
5326
- if (!value) continue;
5327
- const key = value;
5328
- if (seen.has(key)) continue;
5329
- seen.add(key);
5330
- result.push(value);
5331
- }
5332
- return result;
5333
- },
5334
-
5335
- normalizeEnvItems(items) {
5336
- if (!Array.isArray(items)) {
5337
- return { ok: true, items: {} };
5338
- }
5339
- const output = {};
5340
- const seen = new Set();
5341
- for (const item of items) {
5342
- const key = item && typeof item.key === 'string' ? item.key.trim() : '';
5343
- if (!key) continue;
5344
- if (seen.has(key)) {
5345
- return { ok: false, error: `环境变量重复: ${key}` };
5346
- }
5347
- seen.add(key);
5348
- const value = item && typeof item.value !== 'undefined' ? String(item.value) : '';
5349
- output[key] = value;
5350
- }
5351
- return { ok: true, items: output };
5352
- },
5353
-
5354
- parseOptionalNumber(value, label) {
5355
- const text = typeof value === 'string' ? value.trim() : String(value || '').trim();
5356
- if (!text) {
5357
- return { ok: true, value: null };
5358
- }
5359
- const num = Number(text);
5360
- if (!Number.isFinite(num) || num < 0) {
5361
- return { ok: false, error: `${label} 请输入有效数字` };
5362
- }
5363
- return { ok: true, value: num };
5364
- },
5365
-
5366
- applyOpenclawStructuredToText() {
5367
- const parsed = this.parseOpenclawContent(this.openclawEditing.content, { allowEmpty: true });
5368
- if (!parsed.ok) {
5369
- this.showMessage('解析 OpenClaw 配置失败: ' + parsed.error, 'error');
5370
- return;
5371
- }
5372
-
5373
- const config = parsed.data;
5374
- const agents = config.agents && typeof config.agents === 'object' && !Array.isArray(config.agents)
5375
- ? config.agents
5376
- : {};
5377
- const defaults = agents.defaults && typeof agents.defaults === 'object' && !Array.isArray(agents.defaults)
5378
- ? agents.defaults
5379
- : {};
5380
- const model = defaults.model && typeof defaults.model === 'object' && !Array.isArray(defaults.model)
5381
- ? defaults.model
5382
- : {};
5383
-
5384
- const primary = (this.openclawStructured.agentPrimary || '').trim();
5385
- const fallbacks = this.normalizeStringList(this.openclawStructured.agentFallbacks);
5386
- if (primary) {
5387
- model.primary = primary;
5388
- }
5389
- if (fallbacks.length) {
5390
- model.fallbacks = fallbacks;
5391
- }
5392
- if (primary || fallbacks.length) {
5393
- defaults.model = model;
5394
- }
5395
- if (primary && config.agent && typeof config.agent === 'object' && !Array.isArray(config.agent)) {
5396
- config.agent.model = primary;
5397
- }
5398
-
5399
- const workspace = (this.openclawStructured.workspace || '').trim();
5400
- if (workspace) {
5401
- defaults.workspace = workspace;
5402
- }
5403
-
5404
- const timeout = this.parseOptionalNumber(this.openclawStructured.timeout, 'Timeout');
5405
- if (!timeout.ok) {
5406
- this.showMessage(timeout.error, 'error');
5407
- return;
5408
- }
5409
- if (timeout.value !== null) {
5410
- defaults.timeout = timeout.value;
5411
- }
5412
-
5413
- const contextTokens = this.parseOptionalNumber(this.openclawStructured.contextTokens, 'Context Tokens');
5414
- if (!contextTokens.ok) {
5415
- this.showMessage(contextTokens.error, 'error');
5416
- return;
5417
- }
5418
- if (contextTokens.value !== null) {
5419
- defaults.contextTokens = contextTokens.value;
5420
- }
5421
-
5422
- const maxConcurrent = this.parseOptionalNumber(this.openclawStructured.maxConcurrent, 'Max Concurrent');
5423
- if (!maxConcurrent.ok) {
5424
- this.showMessage(maxConcurrent.error, 'error');
5425
- return;
5426
- }
5427
- if (maxConcurrent.value !== null) {
5428
- defaults.maxConcurrent = maxConcurrent.value;
5429
- }
5430
-
5431
- if (Object.keys(defaults).length > 0) {
5432
- config.agents = agents;
5433
- config.agents.defaults = defaults;
5434
- }
5435
-
5436
- const envResult = this.normalizeEnvItems(this.openclawStructured.envItems);
5437
- if (!envResult.ok) {
5438
- this.showMessage(envResult.error, 'error');
5439
- return;
5440
- }
5441
- if (Object.keys(envResult.items).length > 0) {
5442
- config.env = envResult.items;
5443
- } else if (config.env) {
5444
- delete config.env;
5445
- }
5446
-
5447
- const profile = (this.openclawStructured.toolsProfile || '').trim();
5448
- const allowList = this.normalizeStringList(this.openclawStructured.toolsAllow);
5449
- const denyList = this.normalizeStringList(this.openclawStructured.toolsDeny);
5450
- const hasTools = profile || allowList.length || denyList.length || (config.tools && typeof config.tools === 'object');
5451
- if (hasTools) {
5452
- const tools = config.tools && typeof config.tools === 'object' && !Array.isArray(config.tools)
5453
- ? config.tools
5454
- : {};
5455
- tools.profile = profile || tools.profile || 'default';
5456
- tools.allow = allowList;
5457
- tools.deny = denyList;
5458
- config.tools = tools;
5459
- }
5460
-
5461
- this.openclawEditing.content = this.stringifyOpenclawConfig(config);
5462
- this.refreshOpenclawProviders(config);
5463
- this.refreshOpenclawAgentsList(config);
5464
- this.fillOpenclawQuickFromConfig(config);
5465
- this.showMessage('已写入编辑器', 'success');
5466
- },
5467
-
5468
- applyOpenclawQuickToText() {
5469
- const parsed = this.parseOpenclawContent(this.openclawEditing.content, { allowEmpty: true });
5470
- if (!parsed.ok) {
5471
- this.showMessage('解析 OpenClaw 配置失败: ' + parsed.error, 'error');
5472
- return;
5473
- }
5474
-
5475
- const providerName = (this.openclawQuick.providerName || '').trim();
5476
- const modelId = (this.openclawQuick.modelId || '').trim();
5477
- if (!providerName) {
5478
- this.showMessage('请填写 Provider 名称', 'error');
5479
- return;
5480
- }
5481
- if (providerName.includes('/')) {
5482
- this.showMessage('Provider 名称不能包含 "/"', 'error');
5483
- return;
5484
- }
5485
- if (!modelId) {
5486
- this.showMessage('请填写模型 ID', 'error');
5487
- return;
5488
- }
5489
-
5490
- const config = parsed.data;
5491
- const ensureObject = (value) => (value && typeof value === 'object' && !Array.isArray(value)) ? value : {};
5492
- const models = ensureObject(config.models);
5493
- const providers = ensureObject(models.providers);
5494
- const provider = ensureObject(providers[providerName]);
5495
- const baseUrl = (this.openclawQuick.baseUrl || '').trim();
5496
- if (!baseUrl && !provider.baseUrl) {
5497
- this.showMessage('请填写 Base URL', 'error');
5498
- return;
5499
- }
5500
-
5501
- const contextWindow = this.parseOptionalNumber(this.openclawQuick.contextWindow, '上下文长度');
5502
- if (!contextWindow.ok) {
5503
- this.showMessage(contextWindow.error, 'error');
5504
- return;
5505
- }
5506
- const maxTokens = this.parseOptionalNumber(this.openclawQuick.maxTokens, '最大输出');
5507
- if (!maxTokens.ok) {
5508
- this.showMessage(maxTokens.error, 'error');
5509
- return;
5510
- }
5511
-
5512
- const shouldOverrideProvider = !!this.openclawQuick.overrideProvider;
5513
- const apiKey = (this.openclawQuick.apiKey || '').trim();
5514
- const apiType = (this.openclawQuick.apiType || '').trim();
5515
- const setProviderField = (key, value) => {
5516
- if (!value) return;
5517
- if (shouldOverrideProvider || provider[key] === undefined || provider[key] === null || provider[key] === '') {
5518
- provider[key] = value;
5519
- }
5520
- };
5521
- setProviderField('baseUrl', baseUrl);
5522
- setProviderField('api', apiType);
5523
- if (apiKey) {
5524
- setProviderField('apiKey', apiKey);
5525
- }
5526
-
5527
- const modelName = (this.openclawQuick.modelName || '').trim() || modelId;
5528
- const modelEntry = {
5529
- id: modelId,
5530
- name: modelName,
5531
- reasoning: false,
5532
- input: ['text'],
5533
- cost: {
5534
- input: 0,
5535
- output: 0,
5536
- cacheRead: 0,
5537
- cacheWrite: 0
5538
- }
5539
- };
5540
- if (contextWindow.value !== null) {
5541
- modelEntry.contextWindow = contextWindow.value;
5542
- }
5543
- if (maxTokens.value !== null) {
5544
- modelEntry.maxTokens = maxTokens.value;
5545
- }
5546
-
5547
- const existingModels = Array.isArray(provider.models) ? [...provider.models] : [];
5548
- if (this.openclawQuick.overrideModels || existingModels.length === 0) {
5549
- provider.models = [modelEntry];
5550
- } else {
5551
- const idx = existingModels.findIndex(item => item && item.id === modelId);
5552
- if (idx >= 0) {
5553
- existingModels[idx] = this.mergeOpenclawModelEntry(existingModels[idx], modelEntry, false);
5554
- } else {
5555
- existingModels.push(modelEntry);
5556
- }
5557
- provider.models = existingModels;
5558
- }
5559
-
5560
- providers[providerName] = provider;
5561
- models.providers = providers;
5562
- config.models = models;
5563
-
5564
- if (this.openclawQuick.setPrimary) {
5565
- const agents = ensureObject(config.agents);
5566
- const defaults = ensureObject(agents.defaults);
5567
- const modelConfig = defaults.model && typeof defaults.model === 'object' && !Array.isArray(defaults.model)
5568
- ? defaults.model
5569
- : {};
5570
- modelConfig.primary = `${providerName}/${modelId}`;
5571
- defaults.model = modelConfig;
5572
- agents.defaults = defaults;
5573
- config.agents = agents;
5574
- if (config.agent && typeof config.agent === 'object' && !Array.isArray(config.agent)) {
5575
- config.agent.model = modelConfig.primary;
5576
- }
5577
- }
5578
-
5579
- this.openclawEditing.content = this.stringifyOpenclawConfig(config);
5580
- this.fillOpenclawStructured(config);
5581
- this.refreshOpenclawProviders(config);
5582
- this.refreshOpenclawAgentsList(config);
5583
- this.showMessage('快速配置已写入编辑器', 'success');
5584
- },
5585
-
5586
- addOpenclawFallback() {
5587
- this.openclawStructured.agentFallbacks.push('');
5588
- },
5589
-
5590
- removeOpenclawFallback(index) {
5591
- this.openclawStructured.agentFallbacks.splice(index, 1);
5592
- if (this.openclawStructured.agentFallbacks.length === 0) {
5593
- this.openclawStructured.agentFallbacks.push('');
5594
- }
5595
- },
5596
-
5597
- addOpenclawEnvItem() {
5598
- this.openclawStructured.envItems.push({ key: '', value: '', show: false });
5599
- },
5600
-
5601
- removeOpenclawEnvItem(index) {
5602
- this.openclawStructured.envItems.splice(index, 1);
5603
- if (this.openclawStructured.envItems.length === 0) {
5604
- this.openclawStructured.envItems.push({ key: '', value: '', show: false });
5605
- }
5606
- },
5607
-
5608
- toggleOpenclawEnvItem(index) {
5609
- const item = this.openclawStructured.envItems[index];
5610
- if (item) {
5611
- item.show = !item.show;
5612
- }
5613
- },
5614
-
5615
- addOpenclawToolsAllow() {
5616
- this.openclawStructured.toolsAllow.push('');
5617
- },
5618
-
5619
- removeOpenclawToolsAllow(index) {
5620
- this.openclawStructured.toolsAllow.splice(index, 1);
5621
- if (this.openclawStructured.toolsAllow.length === 0) {
5622
- this.openclawStructured.toolsAllow.push('');
5623
- }
5624
- },
5625
-
5626
- addOpenclawToolsDeny() {
5627
- this.openclawStructured.toolsDeny.push('');
5628
- },
5629
-
5630
- removeOpenclawToolsDeny(index) {
5631
- this.openclawStructured.toolsDeny.splice(index, 1);
5632
- if (this.openclawStructured.toolsDeny.length === 0) {
5633
- this.openclawStructured.toolsDeny.push('');
5634
- }
5635
- },
5636
-
5637
- openclawHasContent(config) {
5638
- return !!(config && typeof config.content === 'string' && config.content.trim());
5639
- },
5640
-
5641
- openclawSubtitle(config) {
5642
- if (!this.openclawHasContent(config)) {
5643
- return '未设置配置';
5644
- }
5645
- const length = config.content.trim().length;
5646
- return `已保存 ${length} 字符`;
5647
- },
5648
-
5649
- saveOpenclawConfigs() {
5650
- localStorage.setItem('openclawConfigs', JSON.stringify(this.openclawConfigs));
5651
- },
5652
-
5653
- openOpenclawAddModal() {
5654
- this.openclawEditorTitle = '添加 OpenClaw 配置';
5655
- this.openclawEditing = {
5656
- name: '',
5657
- content: '',
5658
- lockName: false
5659
- };
5660
- this.openclawConfigPath = '';
5661
- this.openclawConfigExists = false;
5662
- this.openclawLineEnding = '\n';
5663
- void this.loadOpenclawConfigFromFile({ silent: true, force: true, fallbackToTemplate: true });
5664
- this.showOpenclawConfigModal = true;
5665
- },
5666
-
5667
- openOpenclawEditModal(name) {
5668
- this.openclawEditorTitle = `编辑 OpenClaw 配置: ${name}`;
5669
- this.openclawEditing = {
5670
- name,
5671
- content: '',
5672
- lockName: true
5673
- };
5674
- void this.loadOpenclawConfigFromFile({ silent: true, force: true, fallbackToTemplate: true });
5675
- this.showOpenclawConfigModal = true;
5676
- },
5677
-
5678
- closeOpenclawConfigModal() {
5679
- this.showOpenclawConfigModal = false;
5680
- this.openclawEditing = { name: '', content: '', lockName: false };
5681
- this.openclawSaving = false;
5682
- this.openclawApplying = false;
5683
- this.resetOpenclawStructured();
5684
- this.resetOpenclawQuick();
5685
- },
5686
-
5687
- async loadOpenclawConfigFromFile(options = {}) {
5688
- const silent = !!options.silent;
5689
- const force = !!options.force;
5690
- const fallbackToTemplate = options.fallbackToTemplate !== false;
5691
- this.openclawFileLoading = true;
5692
- try {
5693
- const res = await api('get-openclaw-config');
5694
- if (res.error) {
5695
- if (!silent) {
5696
- this.showMessage(res.error, 'error');
5697
- }
5698
- return;
5699
- }
5700
- this.openclawConfigPath = res.path || '';
5701
- this.openclawConfigExists = !!res.exists;
5702
- this.openclawLineEnding = res.lineEnding === '\r\n' ? '\r\n' : '\n';
5703
- const hasContent = !!(res.content && res.content.trim());
5704
- const shouldOverride = force || !this.openclawEditing.content || !this.openclawEditing.content.trim();
5705
- if (hasContent && shouldOverride) {
5706
- this.openclawEditing.content = res.content;
5707
- } else if (!hasContent && shouldOverride && fallbackToTemplate) {
5708
- this.openclawEditing.content = DEFAULT_OPENCLAW_TEMPLATE;
5709
- }
5710
- this.syncOpenclawStructuredFromText({ silent: true });
5711
- if (!silent) {
5712
- this.showMessage('已加载当前 OpenClaw 配置', 'success');
5713
- }
5714
- } catch (e) {
5715
- if (!silent) {
5716
- this.showMessage('加载 OpenClaw 配置失败: ' + e.message, 'error');
5717
- }
5718
- } finally {
5719
- this.openclawFileLoading = false;
5720
- }
5721
- },
5722
-
5723
- persistOpenclawConfig({ closeModal = true } = {}) {
5724
- if (!this.openclawEditing.name || !this.openclawEditing.name.trim()) {
5725
- this.showMessage('请输入配置名称', 'error');
5726
- return '';
5727
- }
5728
- const name = this.openclawEditing.name.trim();
5729
- if (!this.openclawEditing.lockName && this.openclawConfigs[name]) {
5730
- this.showMessage('配置名称已存在', 'error');
5731
- return '';
5732
- }
5733
- if (!this.openclawEditing.content || !this.openclawEditing.content.trim()) {
5734
- this.showMessage('配置内容不能为空', 'error');
5735
- return '';
5736
- }
5737
-
5738
- this.openclawConfigs[name] = {
5739
- content: this.openclawEditing.content
5740
- };
5741
- this.currentOpenclawConfig = name;
5742
- this.saveOpenclawConfigs();
5743
- if (closeModal) {
5744
- this.closeOpenclawConfigModal();
5745
- }
5746
- return name;
5747
- },
5748
-
5749
- async saveOpenclawConfig() {
5750
- this.openclawSaving = true;
5751
- try {
5752
- const name = this.persistOpenclawConfig();
5753
- if (!name) return;
5754
- this.showMessage('OpenClaw 配置已保存', 'success');
5755
- } finally {
5756
- this.openclawSaving = false;
5757
- }
5758
- },
5759
-
5760
- async saveAndApplyOpenclawConfig() {
5761
- this.openclawApplying = true;
5762
- try {
5763
- const name = this.persistOpenclawConfig({ closeModal: false });
5764
- if (!name) return;
5765
- const config = this.openclawConfigs[name];
5766
- const res = await api('apply-openclaw-config', {
5767
- content: config.content,
5768
- lineEnding: this.openclawLineEnding
5769
- });
5770
- if (res.error || res.success === false) {
5771
- this.showMessage(res.error || '应用 OpenClaw 配置失败', 'error');
5772
- return;
5773
- }
5774
- this.openclawConfigPath = res.targetPath || this.openclawConfigPath;
5775
- this.openclawConfigExists = true;
5776
- const targetTip = res.targetPath ? `(${res.targetPath})` : '';
5777
- this.showMessage(`已保存并应用 OpenClaw 配置${targetTip}`, 'success');
5778
- this.closeOpenclawConfigModal();
5779
- } catch (e) {
5780
- this.showMessage('应用 OpenClaw 配置失败: ' + e.message, 'error');
5781
- } finally {
5782
- this.openclawApplying = false;
5783
- }
5784
- },
5785
-
5786
- deleteOpenclawConfig(name) {
5787
- if (Object.keys(this.openclawConfigs).length <= 1) {
5788
- return this.showMessage('至少保留一个配置', 'error');
5789
- }
5790
- if (!confirm(`确定删除配置 "${name}"?`)) return;
5791
- delete this.openclawConfigs[name];
5792
- if (this.currentOpenclawConfig === name) {
5793
- this.currentOpenclawConfig = Object.keys(this.openclawConfigs)[0];
5794
- }
5795
- this.saveOpenclawConfigs();
5796
- this.showMessage('OpenClaw 配置已删除', 'success');
5797
- },
5798
-
5799
- async applyOpenclawConfig(name) {
5800
- this.currentOpenclawConfig = name;
5801
- const config = this.openclawConfigs[name];
5802
- if (!this.openclawHasContent(config)) {
5803
- return this.showMessage('该配置为空,请先编辑', 'error');
5804
- }
5805
- const res = await api('apply-openclaw-config', {
5806
- content: config.content,
5807
- lineEnding: this.openclawLineEnding
5808
- });
5809
- if (res.error || res.success === false) {
5810
- this.showMessage(res.error || '应用 OpenClaw 配置失败', 'error');
5811
- } else {
5812
- this.openclawConfigPath = res.targetPath || this.openclawConfigPath;
5813
- this.openclawConfigExists = true;
5814
- const targetTip = res.targetPath ? `(${res.targetPath})` : '';
5815
- this.showMessage(`已应用 OpenClaw 配置: ${name}${targetTip}`, 'success');
5816
- }
5817
- },
5818
-
5819
- formatLatency(result) {
5820
- if (!result) return '';
5821
- if (!result.ok) return result.status ? `ERR ${result.status}` : 'ERR';
5822
- const ms = typeof result.durationMs === 'number' ? result.durationMs : 0;
5823
- return `${ms}ms`;
5824
- },
5825
-
5826
- buildSpeedTestIssue(name, result) {
5827
- if (!name || !result) return null;
5828
- if (result.error) {
5829
- const error = String(result.error || '');
5830
- const errorLower = error.toLowerCase();
5831
- if (error === 'Provider not found') {
5832
- return {
5833
- code: 'remote-speedtest-provider-missing',
5834
- message: `提供商 ${name} 未找到,无法测速`,
5835
- suggestion: '检查配置是否存在该 provider'
5836
- };
5837
- }
5838
- if (error === 'Provider missing URL' || error === 'Missing name or url') {
5839
- return {
5840
- code: 'remote-speedtest-baseurl-missing',
5841
- message: `提供商 ${name} 缺少 base_url`,
5842
- suggestion: '补全 base_url 后重试'
5843
- };
5844
- }
5845
- if (errorLower.includes('invalid url')) {
5846
- return {
5847
- code: 'remote-speedtest-invalid-url',
5848
- message: `提供商 ${name} 的 base_url 无效`,
5849
- suggestion: '请设置为 http/https 的完整 URL'
5850
- };
5851
- }
5852
- if (errorLower.includes('timeout')) {
5853
- return {
5854
- code: 'remote-speedtest-timeout',
5855
- message: `提供商 ${name} 远程测速超时`,
5856
- suggestion: '检查网络或 base_url 是否可达'
5857
- };
5858
- }
5859
- return {
5860
- code: 'remote-speedtest-unreachable',
5861
- message: `提供商 ${name} 远程测速失败:${error || '无法连接'}`,
5862
- suggestion: '检查网络或 base_url 是否可用'
5863
- };
5864
- }
5865
-
5866
- const status = typeof result.status === 'number' ? result.status : 0;
5867
- if (status === 401 || status === 403) {
5868
- return {
5869
- code: 'remote-speedtest-auth-failed',
5870
- message: `提供商 ${name} 远程测速鉴权失败(401/403)`,
5871
- suggestion: '检查 API Key 或认证方式'
5872
- };
5873
- }
5874
- if (status >= 400) {
5875
- return {
5876
- code: 'remote-speedtest-http-error',
5877
- message: `提供商 ${name} 远程测速返回异常状态: ${status}`,
5878
- suggestion: '检查 base_url 或服务状态'
5879
- };
5880
- }
5881
- return null;
5882
- },
5883
-
5884
- async runSpeedTest(name, options = {}) {
5885
- if (!name || this.speedLoading[name]) return null;
5886
- const silent = !!options.silent;
5887
- this.speedLoading[name] = true;
5888
- try {
5889
- const res = await api('speed-test', { name });
5890
- if (res.error) {
5891
- this.speedResults[name] = { ok: false, error: res.error };
5892
- if (!silent) {
5893
- this.showMessage(res.error, 'error');
5894
- }
5895
- return { ok: false, error: res.error };
5896
- }
5897
- this.speedResults[name] = res;
5898
- if (!silent) {
5899
- const status = res.status ? ` (${res.status})` : '';
5900
- this.showMessage(`Speed ${name}: ${this.formatLatency(res)}${status}`, 'success');
5901
- }
5902
- return res;
5903
- } catch (e) {
5904
- const message = e && e.message ? e.message : 'Speed test failed';
5905
- this.speedResults[name] = { ok: false, error: message };
5906
- if (!silent) {
5907
- this.showMessage(message, 'error');
5908
- }
5909
- return { ok: false, error: message };
5910
- } finally {
5911
- this.speedLoading[name] = false;
5912
- }
5913
- },
5914
-
5915
- showMessage(text, type) {
5916
- this.message = text;
5917
- this.messageType = type || 'info';
5918
- setTimeout(() => {
5919
- this.message = '';
5920
- }, 3000);
5921
- }
5922
- }
5923
- });
5924
-
5925
- app.mount('#app');
5926
- </script>
3975
+ <script type="module" src="web-ui/app.js"></script>
5927
3976
  </body>
5928
3977
  </html>