codexmate 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/web-ui.html ADDED
@@ -0,0 +1,3183 @@
1
+ <!DOCTYPE html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Codex Mate</title>
7
+ <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
8
+ <style>
9
+ @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&family=Source+Sans+3:wght@400;500;600&family=Space+Grotesk:wght@400;500;600;700&display=swap');
10
+
11
+ /* ============================================
12
+ 设计系统 - Design Tokens
13
+ ============================================ */
14
+ :root {
15
+ /* 色彩系统 */
16
+ --color-brand: #C95E4B;
17
+ --color-brand-dark: #A94637;
18
+ --color-brand-light: rgba(201, 94, 75, 0.14);
19
+ --color-brand-subtle: rgba(201, 94, 75, 0.2);
20
+
21
+ --color-bg: #F6EFE6;
22
+ --color-surface: #FFFDF9;
23
+ --color-surface-alt: #FFF7F0;
24
+ --color-surface-elevated: #FFFFFF;
25
+ --color-surface-tint: rgba(255, 255, 255, 0.78);
26
+ --color-text-primary: #1B1714;
27
+ --color-text-secondary: #4B4038;
28
+ --color-text-tertiary: #7A6A5D;
29
+ --color-text-muted: #6C5B50;
30
+ --color-border: #D8C9B8;
31
+ --color-border-soft: rgba(216, 201, 184, 0.45);
32
+ --color-border-strong: rgba(216, 201, 184, 0.8);
33
+
34
+ --color-success: #4E8B66;
35
+ --color-error: #C1483B;
36
+
37
+ --bg-warm-gradient:
38
+ radial-gradient(circle at 16% 10%, rgba(201, 94, 75, 0.18), transparent 45%),
39
+ radial-gradient(circle at 90% 6%, rgba(255, 255, 255, 0.85), transparent 40%),
40
+ linear-gradient(135deg, #F6EFE6 0%, #EFE1D4 45%, #F6EFE6 100%);
41
+
42
+ /* 字体系统 */
43
+ --font-family-body: 'Source Sans 3', 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', sans-serif;
44
+ --font-family-display: 'Space Grotesk', 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', sans-serif;
45
+ --font-family-mono: 'JetBrains Mono', 'SFMono-Regular', Consolas, 'Liberation Mono', monospace;
46
+ --font-family: var(--font-family-body);
47
+
48
+ --font-size-display: 52px;
49
+ --font-size-title: 18px;
50
+ --font-size-body: 15px;
51
+ --font-size-secondary: 13px;
52
+ --font-size-caption: 11px;
53
+
54
+ --font-weight-display: 600;
55
+ --font-weight-title: 600;
56
+ --font-weight-body: 400;
57
+ --font-weight-secondary: 500;
58
+ --font-weight-caption: 500;
59
+
60
+ --line-height-tight: 1.12;
61
+ --line-height-normal: 1.5;
62
+
63
+ /* 间距系统 */
64
+ --spacing-xs: 8px;
65
+ --spacing-sm: 16px;
66
+ --spacing-md: 24px;
67
+ --spacing-lg: 40px;
68
+ --spacing-xl: 64px;
69
+
70
+ /* 圆角系统 */
71
+ --radius-sm: 8px;
72
+ --radius-lg: 12px;
73
+ --radius-xl: 18px;
74
+ --radius-full: 50px;
75
+
76
+ /* 阴影系统 - 多层叠加提升真实感 */
77
+ --shadow-subtle:
78
+ 0 1px 2px rgba(31, 26, 23, 0.04),
79
+ 0 1px 3px rgba(31, 26, 23, 0.02);
80
+ --shadow-card:
81
+ 0 1px 3px rgba(31, 26, 23, 0.04),
82
+ 0 4px 12px rgba(31, 26, 23, 0.03);
83
+ --shadow-card-hover:
84
+ 0 2px 4px rgba(31, 26, 23, 0.04),
85
+ 0 8px 20px rgba(31, 26, 23, 0.06);
86
+ --shadow-float:
87
+ 0 6px 16px rgba(31, 26, 23, 0.08),
88
+ 0 18px 36px rgba(31, 26, 23, 0.06);
89
+ --shadow-raised:
90
+ 0 4px 12px rgba(31, 26, 23, 0.06),
91
+ 0 12px 32px rgba(31, 26, 23, 0.04);
92
+ --shadow-modal:
93
+ 0 8px 24px rgba(31, 26, 23, 0.08),
94
+ 0 24px 64px rgba(31, 26, 23, 0.06);
95
+ --shadow-input-focus:
96
+ 0 0 0 3px var(--color-brand-light),
97
+ 0 1px 3px rgba(31, 26, 23, 0.04);
98
+
99
+ /* 动画 - 更细腻的曲线 */
100
+ --transition-instant: 100ms;
101
+ --transition-fast: 150ms;
102
+ --transition-normal: 250ms;
103
+ --transition-slow: 350ms;
104
+ --ease-spring: cubic-bezier(0.16, 1, 0.3, 1);
105
+ --ease-spring-soft: cubic-bezier(0.25, 1, 0.5, 1);
106
+ --ease-smooth: cubic-bezier(0.4, 0, 0.2, 1);
107
+ --ease-out-expo: cubic-bezier(0.19, 1, 0.22, 1);
108
+ }
109
+
110
+ /* ============================================
111
+ 基础重置
112
+ ============================================ */
113
+ * {
114
+ margin: 0;
115
+ padding: 0;
116
+ box-sizing: border-box;
117
+ }
118
+
119
+ body {
120
+ font-family: var(--font-family-body);
121
+ background-color: var(--color-bg);
122
+ background: var(--bg-warm-gradient);
123
+ color: var(--color-text-primary);
124
+ display: flex;
125
+ justify-content: center;
126
+ align-items: center;
127
+ min-height: 100vh;
128
+ padding: var(--spacing-lg) var(--spacing-md);
129
+ -webkit-font-smoothing: antialiased;
130
+ -moz-osx-font-smoothing: grayscale;
131
+ position: relative;
132
+ overflow-x: hidden;
133
+ }
134
+
135
+ body::before {
136
+ content: "";
137
+ position: fixed;
138
+ inset: 0;
139
+ background-image:
140
+ radial-gradient(circle at 12% 18%, rgba(201, 94, 75, 0.1), transparent 38%),
141
+ repeating-linear-gradient(90deg, rgba(255, 255, 255, 0.4) 0 1px, transparent 1px 120px),
142
+ repeating-linear-gradient(0deg, rgba(255, 255, 255, 0.35) 0 1px, transparent 1px 120px);
143
+ opacity: 0.45;
144
+ pointer-events: none;
145
+ z-index: 0;
146
+ }
147
+
148
+ /* ============================================
149
+ 容器
150
+ ============================================ */
151
+ .container {
152
+ width: 100%;
153
+ max-width: 1160px;
154
+ margin: 0 auto;
155
+ padding-bottom: var(--spacing-lg);
156
+ position: relative;
157
+ z-index: 1;
158
+ }
159
+
160
+ /* ============================================
161
+ 主标题
162
+ ============================================ */
163
+ .main-title {
164
+ font-size: var(--font-size-display);
165
+ font-weight: var(--font-weight-display);
166
+ line-height: var(--line-height-tight);
167
+ letter-spacing: -0.03em;
168
+ margin-bottom: 10px;
169
+ color: var(--color-text-primary);
170
+ font-family: var(--font-family-display);
171
+ background: linear-gradient(135deg, var(--color-text-primary) 0%, rgba(27, 23, 20, 0.78) 100%);
172
+ -webkit-background-clip: text;
173
+ -webkit-text-fill-color: transparent;
174
+ background-clip: text;
175
+ }
176
+
177
+ .main-title .accent {
178
+ color: var(--color-brand);
179
+ -webkit-text-fill-color: var(--color-brand);
180
+ position: relative;
181
+ }
182
+
183
+ .subtitle {
184
+ font-size: var(--font-size-body);
185
+ color: var(--color-text-tertiary);
186
+ line-height: var(--line-height-normal);
187
+ margin-bottom: 20px;
188
+ max-width: 640px;
189
+ letter-spacing: 0.01em;
190
+ }
191
+
192
+ /* ============================================
193
+ 模式切换器 - Segmented Control
194
+ ============================================ */
195
+ .segmented-control {
196
+ display: flex;
197
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.85) 0%, rgba(255, 255, 255, 0.55) 100%);
198
+ border-radius: var(--radius-lg);
199
+ padding: 5px;
200
+ margin-bottom: 16px;
201
+ position: relative;
202
+ box-shadow: inset 0 1px 2px rgba(31, 26, 23, 0.08);
203
+ border: 1px solid rgba(255, 255, 255, 0.7);
204
+ backdrop-filter: blur(10px);
205
+ }
206
+
207
+ .segment {
208
+ flex: 1;
209
+ padding: 11px 16px;
210
+ border: none;
211
+ background: transparent;
212
+ font-size: var(--font-size-body);
213
+ font-weight: var(--font-weight-secondary);
214
+ color: var(--color-text-secondary);
215
+ cursor: pointer;
216
+ border-radius: 10px;
217
+ transition: all var(--transition-normal) var(--ease-spring);
218
+ position: relative;
219
+ z-index: 2;
220
+ letter-spacing: 0.01em;
221
+ }
222
+
223
+ .segment:hover {
224
+ color: var(--color-text-primary);
225
+ }
226
+
227
+ .segment.active {
228
+ color: var(--color-text-primary);
229
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.95) 0%, rgba(255, 255, 255, 0.8) 100%);
230
+ box-shadow: var(--shadow-subtle), inset 0 1px 0 rgba(255, 255, 255, 0.85);
231
+ }
232
+
233
+ /* ============================================
234
+ 卡片列表
235
+ ============================================ */
236
+ .card-list {
237
+ display: flex;
238
+ flex-direction: column;
239
+ gap: 12px;
240
+ margin-bottom: 12px;
241
+ }
242
+
243
+ /* ============================================
244
+ 卡片
245
+ ============================================ */
246
+ .card {
247
+ background: linear-gradient(to bottom, var(--color-surface) 0%, rgba(255, 255, 255, 0.95) 100%);
248
+ border-radius: var(--radius-lg);
249
+ padding: var(--spacing-sm);
250
+ display: flex;
251
+ align-items: center;
252
+ justify-content: space-between;
253
+ cursor: pointer;
254
+ transition:
255
+ transform var(--transition-normal) var(--ease-spring),
256
+ box-shadow var(--transition-normal) var(--ease-spring),
257
+ background-color var(--transition-fast) var(--ease-smooth);
258
+ box-shadow: var(--shadow-card);
259
+ user-select: none;
260
+ will-change: transform;
261
+ border: 1px solid var(--color-border-soft);
262
+ position: relative;
263
+ overflow: hidden;
264
+ }
265
+
266
+ .card:hover {
267
+ transform: translateY(-2px) scale(1.005);
268
+ box-shadow: var(--shadow-float);
269
+ }
270
+
271
+ .card::before,
272
+ .card::after {
273
+ content: "";
274
+ position: absolute;
275
+ pointer-events: none;
276
+ }
277
+
278
+ .card::before {
279
+ left: 0;
280
+ top: 10px;
281
+ bottom: 10px;
282
+ width: 3px;
283
+ border-radius: 999px;
284
+ background: transparent;
285
+ transition: background var(--transition-fast) var(--ease-smooth);
286
+ }
287
+
288
+ .card::after {
289
+ inset: 0;
290
+ border-radius: inherit;
291
+ background: linear-gradient(120deg, rgba(255, 255, 255, 0.7) 0%, transparent 55%);
292
+ opacity: 0;
293
+ transition: opacity var(--transition-normal) var(--ease-smooth);
294
+ }
295
+
296
+ .card:active {
297
+ transform: translateY(0) scale(0.995);
298
+ transition: transform var(--transition-instant) var(--ease-smooth);
299
+ }
300
+
301
+ .card.active {
302
+ background: linear-gradient(to bottom, rgba(210, 107, 90, 0.12) 0%, rgba(255, 255, 255, 0.98) 100%);
303
+ border-color: rgba(201, 94, 75, 0.45);
304
+ box-shadow: var(--shadow-float);
305
+ }
306
+
307
+ .card.active::before {
308
+ background: linear-gradient(180deg, rgba(201, 94, 75, 0.95) 0%, rgba(201, 94, 75, 0.35) 100%);
309
+ }
310
+
311
+ .card:hover::after {
312
+ opacity: 0.6;
313
+ }
314
+
315
+ .card.active .card-icon {
316
+ transform: scale(1.05);
317
+ }
318
+
319
+ .card-leading {
320
+ display: flex;
321
+ align-items: center;
322
+ gap: var(--spacing-sm);
323
+ flex: 1;
324
+ min-width: 0;
325
+ }
326
+
327
+ .card-icon {
328
+ width: 40px;
329
+ height: 40px;
330
+ border-radius: var(--radius-sm);
331
+ background: linear-gradient(135deg, rgba(255, 255, 255, 0.9) 0%, rgba(247, 241, 232, 0.65) 100%);
332
+ display: flex;
333
+ align-items: center;
334
+ justify-content: center;
335
+ font-size: var(--font-size-title);
336
+ font-weight: var(--font-weight-title);
337
+ color: var(--color-text-secondary);
338
+ flex-shrink: 0;
339
+ transition: all var(--transition-normal) var(--ease-spring-soft);
340
+ box-shadow: inset 0 1px 2px rgba(255, 255, 255, 0.7);
341
+ }
342
+
343
+ .card.active .card-icon {
344
+ background: linear-gradient(135deg, var(--color-brand) 0%, var(--color-brand-dark) 100%);
345
+ color: white;
346
+ box-shadow: 0 2px 8px rgba(210, 107, 90, 0.3);
347
+ }
348
+
349
+ .card-content {
350
+ display: flex;
351
+ flex-direction: column;
352
+ gap: 2px;
353
+ min-width: 0;
354
+ }
355
+
356
+ .card-title {
357
+ font-size: var(--font-size-body);
358
+ font-weight: var(--font-weight-secondary);
359
+ color: var(--color-text-primary);
360
+ white-space: nowrap;
361
+ overflow: hidden;
362
+ text-overflow: ellipsis;
363
+ letter-spacing: -0.01em;
364
+ }
365
+
366
+ .card-subtitle {
367
+ font-size: var(--font-size-secondary);
368
+ color: var(--color-text-tertiary);
369
+ white-space: nowrap;
370
+ overflow: hidden;
371
+ text-overflow: ellipsis;
372
+ opacity: 0.8;
373
+ }
374
+
375
+ .card-trailing {
376
+ display: flex;
377
+ align-items: center;
378
+ gap: var(--spacing-xs);
379
+ }
380
+
381
+ /* 卡片操作按钮 - hover 显示 */
382
+ .card-actions {
383
+ display: flex;
384
+ gap: 4px;
385
+ opacity: 0;
386
+ transform: translateX(4px);
387
+ transition: all var(--transition-normal) var(--ease-spring);
388
+ }
389
+
390
+ .card:hover .card-actions {
391
+ opacity: 1;
392
+ transform: translateX(0);
393
+ }
394
+
395
+ .card-action-btn {
396
+ width: 28px;
397
+ height: 28px;
398
+ border-radius: var(--radius-sm);
399
+ border: none;
400
+ background: transparent;
401
+ color: var(--color-text-tertiary);
402
+ cursor: pointer;
403
+ display: flex;
404
+ align-items: center;
405
+ justify-content: center;
406
+ transition: all var(--transition-fast) var(--ease-spring);
407
+ }
408
+
409
+ .card-action-btn:hover {
410
+ background: linear-gradient(135deg, var(--color-bg) 0%, rgba(247, 241, 232, 0.8) 100%);
411
+ color: var(--color-text-secondary);
412
+ transform: scale(1.08);
413
+ }
414
+
415
+ .card-action-btn.delete:hover {
416
+ background: linear-gradient(135deg, rgba(200, 74, 58, 0.1) 0%, rgba(200, 74, 58, 0.05) 100%);
417
+ color: var(--color-error);
418
+ }
419
+
420
+ .card-action-btn svg {
421
+ width: 16px;
422
+ height: 16px;
423
+ }
424
+
425
+ /* ============================================
426
+ 状态徽章
427
+ ============================================ */
428
+ .pill {
429
+ padding: 5px 11px;
430
+ border-radius: var(--radius-full);
431
+ font-size: var(--font-size-caption);
432
+ font-weight: var(--font-weight-caption);
433
+ background-color: rgba(255, 255, 255, 0.8);
434
+ color: var(--color-text-tertiary);
435
+ text-transform: uppercase;
436
+ letter-spacing: 0.06em;
437
+ transition: all var(--transition-fast) var(--ease-smooth);
438
+ box-shadow: inset 0 0.5px 1px rgba(0, 0, 0, 0.04);
439
+ }
440
+
441
+ .pill.configured {
442
+ background: linear-gradient(135deg, rgba(90, 139, 106, 0.15) 0%, rgba(90, 139, 106, 0.08) 100%);
443
+ color: var(--color-success);
444
+ box-shadow: inset 0 0.5px 1px rgba(90, 139, 106, 0.2);
445
+ }
446
+
447
+ .pill.empty {
448
+ background: linear-gradient(135deg, rgba(200, 74, 58, 0.1) 0%, rgba(200, 74, 58, 0.05) 100%);
449
+ color: var(--color-error);
450
+ box-shadow: inset 0 0.5px 1px rgba(200, 74, 58, 0.15);
451
+ }
452
+
453
+ .latency {
454
+ padding: 4px 10px;
455
+ border-radius: var(--radius-full);
456
+ font-size: var(--font-size-caption);
457
+ font-weight: var(--font-weight-caption);
458
+ background: var(--color-bg);
459
+ color: var(--color-text-tertiary);
460
+ letter-spacing: 0.02em;
461
+ }
462
+
463
+ .latency.ok {
464
+ color: var(--color-success);
465
+ background: rgba(90, 139, 106, 0.1);
466
+ }
467
+
468
+ .latency.error {
469
+ color: var(--color-error);
470
+ background: rgba(200, 74, 58, 0.08);
471
+ }
472
+
473
+ .card-action-btn.loading svg {
474
+ animation: spin 0.9s linear infinite;
475
+ }
476
+
477
+ /* ============================================
478
+ 图标 - SVG 优化
479
+ ============================================ */
480
+ .icon {
481
+ width: 20px;
482
+ height: 20px;
483
+ flex-shrink: 0;
484
+ stroke-linecap: round;
485
+ stroke-linejoin: round;
486
+ }
487
+
488
+ .icon-chevron-right {
489
+ color: var(--color-text-tertiary);
490
+ opacity: 0.5;
491
+ }
492
+
493
+ /* ============================================
494
+ 选择器 - 用于模型选择
495
+ ============================================ */
496
+ .selector-section {
497
+ background: linear-gradient(to bottom, var(--color-surface) 0%, rgba(255, 255, 255, 0.92) 100%);
498
+ border-radius: var(--radius-lg);
499
+ padding: calc(var(--spacing-sm) + 2px);
500
+ margin-bottom: 16px;
501
+ box-shadow: var(--shadow-card);
502
+ border: 1px solid var(--color-border-soft);
503
+ }
504
+
505
+ .selector-header {
506
+ display: flex;
507
+ justify-content: space-between;
508
+ align-items: center;
509
+ margin-bottom: var(--spacing-xs);
510
+ }
511
+
512
+ .selector-title {
513
+ font-size: var(--font-size-caption);
514
+ font-weight: var(--font-weight-secondary);
515
+ color: var(--color-text-muted);
516
+ text-transform: none;
517
+ letter-spacing: 0.04em;
518
+ opacity: 0.85;
519
+ }
520
+
521
+ .selector-actions {
522
+ display: flex;
523
+ gap: var(--spacing-xs);
524
+ }
525
+
526
+ .btn-icon {
527
+ width: 28px;
528
+ height: 28px;
529
+ border-radius: var(--radius-sm);
530
+ border: none;
531
+ background: linear-gradient(135deg, var(--color-brand) 0%, var(--color-brand-dark) 100%);
532
+ color: white;
533
+ cursor: pointer;
534
+ font-size: 16px;
535
+ display: flex;
536
+ align-items: center;
537
+ justify-content: center;
538
+ transition: all var(--transition-fast) var(--ease-spring);
539
+ box-shadow: 0 2px 4px rgba(210, 107, 90, 0.2);
540
+ }
541
+
542
+ .btn-icon:hover {
543
+ transform: translateY(-1px) scale(1.05);
544
+ box-shadow: 0 4px 8px rgba(210, 107, 90, 0.25);
545
+ }
546
+
547
+ .btn-icon:active {
548
+ transform: translateY(0) scale(0.98);
549
+ }
550
+
551
+ .model-select {
552
+ width: 100%;
553
+ padding: 12px var(--spacing-sm);
554
+ padding-right: 40px;
555
+ border: 1px solid var(--color-border-soft);
556
+ border-radius: var(--radius-sm);
557
+ font-size: var(--font-size-body);
558
+ font-weight: var(--font-weight-body);
559
+ background-color: var(--color-surface-alt);
560
+ color: var(--color-text-primary);
561
+ outline: none;
562
+ cursor: pointer;
563
+ appearance: none;
564
+ 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");
565
+ background-repeat: no-repeat;
566
+ background-position: right 14px center;
567
+ background-size: 12px;
568
+ transition: all var(--transition-fast) var(--ease-smooth);
569
+ box-shadow: inset 0 1px 2px rgba(31, 26, 23, 0.04);
570
+ }
571
+
572
+ .model-select:hover {
573
+ border-color: var(--color-border-strong);
574
+ background-color: var(--color-surface);
575
+ }
576
+
577
+ .model-select:focus {
578
+ background-color: var(--color-surface);
579
+ border-color: var(--color-brand);
580
+ box-shadow: var(--shadow-input-focus);
581
+ }
582
+
583
+ .config-template-hint {
584
+ margin-top: 8px;
585
+ margin-bottom: 10px;
586
+ font-size: var(--font-size-caption);
587
+ color: var(--color-text-tertiary);
588
+ line-height: 1.4;
589
+ }
590
+
591
+ .btn-template-editor {
592
+ width: 100%;
593
+ margin-top: 2px;
594
+ }
595
+
596
+ /* ============================================
597
+ 按钮
598
+ ============================================ */
599
+ .btn-add {
600
+ width: 100%;
601
+ padding: 14px var(--spacing-sm);
602
+ border: 1.5px dashed rgba(208, 196, 182, 0.6);
603
+ border-radius: var(--radius-lg);
604
+ background: linear-gradient(to bottom, rgba(255, 255, 255, 0.55) 0%, rgba(255, 255, 255, 0.15) 100%);
605
+ font-size: var(--font-size-body);
606
+ font-weight: var(--font-weight-secondary);
607
+ color: var(--color-text-tertiary);
608
+ cursor: pointer;
609
+ transition: all var(--transition-normal) var(--ease-spring);
610
+ display: flex;
611
+ align-items: center;
612
+ justify-content: center;
613
+ gap: var(--spacing-xs);
614
+ }
615
+
616
+ .btn-add + .selector-section,
617
+ .selector-section + .btn-add,
618
+ .btn-add + .card-list,
619
+ .card-list + .btn-add {
620
+ margin-top: 12px;
621
+ }
622
+
623
+ .btn-add:hover {
624
+ border-color: var(--color-brand);
625
+ color: var(--color-brand);
626
+ background: linear-gradient(to bottom, rgba(210, 107, 90, 0.05) 0%, rgba(210, 107, 90, 0.02) 100%);
627
+ transform: translateY(-1px);
628
+ }
629
+
630
+ .btn-add:active {
631
+ transform: translateY(0) scale(0.99);
632
+ }
633
+
634
+ .btn-add .icon {
635
+ width: 18px;
636
+ height: 18px;
637
+ transition: transform var(--transition-normal) var(--ease-spring);
638
+ }
639
+
640
+ .btn-add:hover .icon {
641
+ transform: rotate(90deg);
642
+ }
643
+
644
+ .btn-tool {
645
+ padding: 12px var(--spacing-sm);
646
+ border-radius: var(--radius-sm);
647
+ border: 1px solid var(--color-border-soft);
648
+ background: linear-gradient(to bottom, rgba(255, 255, 255, 0.95) 0%, rgba(255, 255, 255, 0.85) 100%);
649
+ font-size: var(--font-size-body);
650
+ font-weight: var(--font-weight-secondary);
651
+ color: var(--color-text-secondary);
652
+ cursor: pointer;
653
+ transition: all var(--transition-fast) var(--ease-spring);
654
+ box-shadow: var(--shadow-subtle);
655
+ letter-spacing: -0.01em;
656
+ }
657
+
658
+ .btn-tool:hover {
659
+ border-color: var(--color-brand);
660
+ color: var(--color-brand);
661
+ transform: translateY(-1px);
662
+ box-shadow: 0 4px 8px rgba(210, 107, 90, 0.12);
663
+ }
664
+
665
+ .session-toolbar {
666
+ display: grid;
667
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
668
+ gap: var(--spacing-xs);
669
+ margin-bottom: var(--spacing-sm);
670
+ align-items: end;
671
+ }
672
+
673
+ .session-toolbar-group {
674
+ display: flex;
675
+ align-items: center;
676
+ gap: var(--spacing-xs);
677
+ flex-wrap: wrap;
678
+ min-width: 0;
679
+ }
680
+
681
+ .session-toolbar-grow {
682
+ grid-column: span 2;
683
+ }
684
+
685
+ .session-toolbar-actions {
686
+ justify-content: flex-end;
687
+ }
688
+
689
+ .session-source-select,
690
+ .session-path-select,
691
+ .session-query-input,
692
+ .session-role-select,
693
+ .session-time-select {
694
+ flex: 1;
695
+ min-width: 160px;
696
+ padding: 10px 12px;
697
+ border-radius: var(--radius-sm);
698
+ border: 1px solid var(--color-border-soft);
699
+ background: linear-gradient(to bottom, var(--color-surface) 0%, rgba(255, 255, 255, 0.92) 100%);
700
+ color: var(--color-text-secondary);
701
+ font-size: var(--font-size-body);
702
+ font-family: var(--font-family);
703
+ outline: none;
704
+ transition: all var(--transition-fast) var(--ease-spring);
705
+ box-shadow: inset 0 1px 2px rgba(31, 26, 23, 0.04);
706
+ }
707
+
708
+ .session-query-input {
709
+ flex: 2;
710
+ min-width: 220px;
711
+ }
712
+
713
+ .session-source-select:hover,
714
+ .session-path-select:hover,
715
+ .session-query-input:hover,
716
+ .session-role-select:hover,
717
+ .session-time-select:hover {
718
+ border-color: var(--color-border-strong);
719
+ }
720
+
721
+ .session-source-select:focus,
722
+ .session-path-select:focus,
723
+ .session-query-input:focus,
724
+ .session-role-select:focus,
725
+ .session-time-select:focus {
726
+ border-color: var(--color-brand);
727
+ box-shadow: var(--shadow-input-focus);
728
+ }
729
+
730
+ .session-hint {
731
+ font-size: var(--font-size-secondary);
732
+ color: var(--color-text-tertiary);
733
+ margin-bottom: 12px;
734
+ line-height: 1.45;
735
+ }
736
+
737
+ .session-card {
738
+ align-items: flex-start;
739
+ cursor: default;
740
+ }
741
+
742
+ .session-card:hover {
743
+ transform: none;
744
+ box-shadow: var(--shadow-card);
745
+ }
746
+
747
+ .session-card .card-leading {
748
+ align-items: flex-start;
749
+ }
750
+
751
+ .session-meta {
752
+ margin-top: 6px;
753
+ font-size: var(--font-size-caption);
754
+ color: var(--color-text-tertiary);
755
+ line-height: 1.4;
756
+ word-break: break-all;
757
+ }
758
+
759
+ .session-actions {
760
+ display: flex;
761
+ gap: var(--spacing-xs);
762
+ align-items: center;
763
+ margin-left: var(--spacing-sm);
764
+ flex-shrink: 0;
765
+ }
766
+
767
+ .session-source {
768
+ font-size: var(--font-size-caption);
769
+ color: var(--color-brand);
770
+ border: 1px solid rgba(210, 107, 90, 0.25);
771
+ border-radius: 999px;
772
+ padding: 2px 8px;
773
+ background: rgba(210, 107, 90, 0.08);
774
+ white-space: nowrap;
775
+ }
776
+
777
+ .session-count-pill {
778
+ font-size: var(--font-size-caption);
779
+ color: var(--color-text-secondary);
780
+ border: 1px solid var(--color-border-soft);
781
+ border-radius: 999px;
782
+ padding: 2px 8px;
783
+ background: rgba(247, 241, 232, 0.7);
784
+ white-space: nowrap;
785
+ margin-left: auto;
786
+ }
787
+
788
+ .btn-session-export {
789
+ border: 1px solid var(--color-border-soft);
790
+ border-radius: var(--radius-sm);
791
+ background: linear-gradient(to bottom, var(--color-surface) 0%, rgba(255, 255, 255, 0.9) 100%);
792
+ color: var(--color-text-secondary);
793
+ padding: 8px 12px;
794
+ font-size: var(--font-size-secondary);
795
+ font-weight: var(--font-weight-secondary);
796
+ cursor: pointer;
797
+ transition: all var(--transition-fast) var(--ease-spring);
798
+ white-space: nowrap;
799
+ box-shadow: var(--shadow-subtle);
800
+ letter-spacing: -0.01em;
801
+ }
802
+
803
+ .btn-session-refresh {
804
+ border: 1px solid var(--color-border-soft);
805
+ border-radius: var(--radius-sm);
806
+ background: linear-gradient(to bottom, var(--color-surface) 0%, rgba(255, 255, 255, 0.9) 100%);
807
+ color: var(--color-text-secondary);
808
+ padding: 8px 12px;
809
+ font-size: var(--font-size-secondary);
810
+ font-weight: var(--font-weight-secondary);
811
+ cursor: pointer;
812
+ transition: all var(--transition-fast) var(--ease-spring);
813
+ white-space: nowrap;
814
+ box-shadow: var(--shadow-subtle);
815
+ letter-spacing: -0.01em;
816
+ }
817
+
818
+ .btn-session-refresh:hover {
819
+ border-color: var(--color-brand);
820
+ color: var(--color-brand);
821
+ transform: translateY(-1px);
822
+ }
823
+
824
+ .btn-session-refresh:disabled {
825
+ opacity: 0.5;
826
+ cursor: not-allowed;
827
+ transform: none;
828
+ }
829
+
830
+ .btn-session-export:hover {
831
+ border-color: var(--color-brand);
832
+ color: var(--color-brand);
833
+ transform: translateY(-1px);
834
+ }
835
+
836
+ .btn-session-export:disabled {
837
+ opacity: 0.5;
838
+ cursor: not-allowed;
839
+ transform: none;
840
+ }
841
+
842
+ .session-empty {
843
+ padding: 28px var(--spacing-sm);
844
+ text-align: center;
845
+ border: 1px dashed var(--color-border-soft);
846
+ border-radius: var(--radius-lg);
847
+ color: var(--color-text-tertiary);
848
+ background: var(--bg-warm-gradient);
849
+ position: relative;
850
+ box-shadow: var(--shadow-subtle);
851
+ }
852
+
853
+ .session-empty::before {
854
+ content: "";
855
+ display: block;
856
+ width: 36px;
857
+ height: 36px;
858
+ border-radius: 50%;
859
+ margin: 0 auto 10px;
860
+ background: rgba(210, 107, 90, 0.12);
861
+ box-shadow: inset 0 0 0 6px rgba(255, 255, 255, 0.7);
862
+ }
863
+
864
+ .session-layout {
865
+ display: grid;
866
+ grid-template-columns: minmax(280px, 360px) minmax(0, 1fr);
867
+ gap: var(--spacing-sm);
868
+ align-items: start;
869
+ height: min(72vh, 760px);
870
+ min-height: 520px;
871
+ }
872
+
873
+ .session-list {
874
+ display: flex;
875
+ flex-direction: column;
876
+ gap: var(--spacing-xs);
877
+ position: sticky;
878
+ top: 12px;
879
+ height: 100%;
880
+ max-height: none;
881
+ overflow-y: auto;
882
+ overflow-x: hidden;
883
+ padding-right: 4px;
884
+ min-width: 0;
885
+ scrollbar-width: thin;
886
+ scrollbar-color: rgba(166, 149, 130, 0.85) transparent;
887
+ }
888
+
889
+ .session-list::-webkit-scrollbar,
890
+ .session-preview-scroll::-webkit-scrollbar {
891
+ width: 10px;
892
+ height: 10px;
893
+ }
894
+
895
+ .session-list::-webkit-scrollbar-track,
896
+ .session-preview-scroll::-webkit-scrollbar-track {
897
+ background: transparent;
898
+ border-radius: 999px;
899
+ }
900
+
901
+ .session-list::-webkit-scrollbar-thumb,
902
+ .session-preview-scroll::-webkit-scrollbar-thumb {
903
+ background: linear-gradient(to bottom, rgba(191, 174, 154, 0.95) 0%, rgba(160, 141, 121, 0.95) 100%);
904
+ border-radius: 999px;
905
+ border: 2px solid rgba(255, 255, 255, 0.9);
906
+ }
907
+
908
+ .session-list::-webkit-scrollbar-thumb:hover,
909
+ .session-preview-scroll::-webkit-scrollbar-thumb:hover {
910
+ background: linear-gradient(to bottom, rgba(175, 156, 136, 0.95) 0%, rgba(145, 126, 107, 0.95) 100%);
911
+ }
912
+
913
+ .session-item {
914
+ border: 1px solid var(--color-border-soft);
915
+ border-radius: var(--radius-sm);
916
+ background: linear-gradient(to bottom, var(--color-surface) 0%, rgba(255, 255, 255, 0.92) 100%);
917
+ padding: 10px 12px;
918
+ cursor: pointer;
919
+ transition: all var(--transition-fast) var(--ease-spring);
920
+ user-select: none;
921
+ min-width: 0;
922
+ }
923
+
924
+ .session-item-header {
925
+ display: flex;
926
+ align-items: flex-start;
927
+ justify-content: space-between;
928
+ gap: 10px;
929
+ margin-bottom: 4px;
930
+ }
931
+
932
+ .session-item-main {
933
+ min-width: 0;
934
+ flex: 1;
935
+ display: flex;
936
+ align-items: flex-start;
937
+ gap: 8px;
938
+ }
939
+
940
+ .session-item:hover {
941
+ border-color: var(--color-brand);
942
+ background: linear-gradient(to bottom, rgba(210, 107, 90, 0.08) 0%, rgba(255, 255, 255, 0.98) 100%);
943
+ transform: translateY(-1px);
944
+ }
945
+
946
+ .session-item:active {
947
+ transform: scale(0.99);
948
+ }
949
+
950
+ .session-item.active {
951
+ border-color: var(--color-brand);
952
+ background: linear-gradient(to bottom, rgba(210, 107, 90, 0.1) 0%, rgba(255, 255, 255, 0.98) 100%);
953
+ box-shadow: 0 6px 16px rgba(210, 107, 90, 0.12);
954
+ }
955
+
956
+ .session-item-title {
957
+ font-size: var(--font-size-body);
958
+ font-weight: var(--font-weight-secondary);
959
+ color: var(--color-text-primary);
960
+ line-height: 1.35;
961
+ display: -webkit-box;
962
+ -webkit-line-clamp: 2;
963
+ -webkit-box-orient: vertical;
964
+ overflow: hidden;
965
+ word-break: break-word;
966
+ flex: 1;
967
+ }
968
+
969
+ .session-item-actions {
970
+ display: inline-flex;
971
+ align-items: center;
972
+ gap: 6px;
973
+ flex-shrink: 0;
974
+ }
975
+
976
+ .session-item-copy {
977
+ border: 1px solid rgba(70, 86, 110, 0.35);
978
+ background: rgba(70, 86, 110, 0.08);
979
+ color: var(--color-text-secondary);
980
+ width: 28px;
981
+ height: 28px;
982
+ border-radius: 8px;
983
+ display: inline-flex;
984
+ align-items: center;
985
+ justify-content: center;
986
+ cursor: pointer;
987
+ flex-shrink: 0;
988
+ transition: all var(--transition-fast) var(--ease-spring);
989
+ }
990
+
991
+ .session-item-copy:hover {
992
+ border-color: rgba(70, 86, 110, 0.7);
993
+ background: rgba(70, 86, 110, 0.16);
994
+ color: var(--color-text-primary);
995
+ transform: translateY(-1px);
996
+ }
997
+
998
+ .session-item-copy:disabled {
999
+ opacity: 0.5;
1000
+ cursor: not-allowed;
1001
+ transform: none;
1002
+ }
1003
+
1004
+ .session-item-copy svg {
1005
+ width: 16px;
1006
+ height: 16px;
1007
+ }
1008
+
1009
+ .session-item-sub {
1010
+ font-size: var(--font-size-caption);
1011
+ color: var(--color-text-tertiary);
1012
+ line-height: 1.35;
1013
+ white-space: nowrap;
1014
+ overflow: hidden;
1015
+ text-overflow: ellipsis;
1016
+ }
1017
+
1018
+ .session-item-sub.session-item-wrap {
1019
+ white-space: normal;
1020
+ }
1021
+
1022
+ .session-item-meta {
1023
+ display: flex;
1024
+ flex-wrap: wrap;
1025
+ align-items: center;
1026
+ gap: 6px;
1027
+ margin-top: 2px;
1028
+ margin-bottom: 2px;
1029
+ }
1030
+
1031
+ .session-item-time {
1032
+ font-size: var(--font-size-caption);
1033
+ color: var(--color-text-tertiary);
1034
+ white-space: nowrap;
1035
+ }
1036
+
1037
+ .session-item-snippet {
1038
+ color: var(--color-text-secondary);
1039
+ white-space: normal;
1040
+ background: rgba(210, 107, 90, 0.08);
1041
+ border-radius: 8px;
1042
+ padding: 6px 8px;
1043
+ border: 1px solid rgba(210, 107, 90, 0.15);
1044
+ }
1045
+
1046
+ .session-preview {
1047
+ border: 1px solid var(--color-border-soft);
1048
+ border-radius: var(--radius-xl);
1049
+ background: linear-gradient(to bottom, var(--color-surface-elevated) 0%, rgba(255, 255, 255, 0.96) 100%);
1050
+ box-shadow: var(--shadow-card);
1051
+ min-height: 0;
1052
+ max-height: none;
1053
+ height: 100%;
1054
+ display: flex;
1055
+ flex-direction: column;
1056
+ overflow: hidden;
1057
+ }
1058
+
1059
+ .session-preview-scroll {
1060
+ flex: 1;
1061
+ min-height: 0;
1062
+ overflow-y: auto;
1063
+ overflow-x: hidden;
1064
+ display: flex;
1065
+ flex-direction: column;
1066
+ scrollbar-width: thin;
1067
+ scrollbar-color: rgba(166, 149, 130, 0.85) transparent;
1068
+ }
1069
+
1070
+ .session-preview-header {
1071
+ padding: 12px var(--spacing-sm);
1072
+ border-bottom: 1px solid var(--color-border-soft);
1073
+ display: flex;
1074
+ align-items: flex-start;
1075
+ justify-content: space-between;
1076
+ gap: var(--spacing-sm);
1077
+ position: sticky;
1078
+ top: 0;
1079
+ z-index: 2;
1080
+ background: linear-gradient(to bottom, rgba(255, 255, 255, 0.98) 0%, rgba(255, 255, 255, 0.92) 100%);
1081
+ backdrop-filter: blur(6px);
1082
+ }
1083
+
1084
+ .session-preview-header > div:first-child {
1085
+ min-width: 0;
1086
+ flex: 1;
1087
+ }
1088
+
1089
+ .session-preview-title {
1090
+ font-size: var(--font-size-body);
1091
+ font-weight: var(--font-weight-title);
1092
+ color: var(--color-text-primary);
1093
+ line-height: 1.4;
1094
+ display: -webkit-box;
1095
+ -webkit-line-clamp: 2;
1096
+ -webkit-box-orient: vertical;
1097
+ overflow: hidden;
1098
+ word-break: break-word;
1099
+ }
1100
+
1101
+ .session-preview-sub {
1102
+ margin-top: 4px;
1103
+ font-size: var(--font-size-caption);
1104
+ color: var(--color-text-tertiary);
1105
+ line-height: 1.35;
1106
+ white-space: nowrap;
1107
+ overflow: hidden;
1108
+ text-overflow: ellipsis;
1109
+ }
1110
+
1111
+ .session-preview-meta {
1112
+ display: flex;
1113
+ flex-wrap: wrap;
1114
+ align-items: center;
1115
+ margin-top: 4px;
1116
+ }
1117
+
1118
+ .session-preview-meta-item {
1119
+ font-size: var(--font-size-caption);
1120
+ color: var(--color-text-tertiary);
1121
+ line-height: 1.35;
1122
+ white-space: nowrap;
1123
+ overflow: hidden;
1124
+ text-overflow: ellipsis;
1125
+ }
1126
+
1127
+ .session-preview-meta-item:not(:last-child)::after {
1128
+ content: "·";
1129
+ margin: 0 6px;
1130
+ color: var(--color-text-tertiary);
1131
+ opacity: 0.7;
1132
+ }
1133
+
1134
+ .session-actions {
1135
+ display: flex;
1136
+ align-items: center;
1137
+ gap: 8px;
1138
+ flex-shrink: 0;
1139
+ margin-left: 0;
1140
+ flex-wrap: wrap;
1141
+ justify-content: flex-end;
1142
+ }
1143
+
1144
+ .session-preview-body {
1145
+ flex: 1;
1146
+ min-height: 0;
1147
+ padding: var(--spacing-sm);
1148
+ display: flex;
1149
+ flex-direction: column;
1150
+ gap: 10px;
1151
+ }
1152
+
1153
+ .session-msg {
1154
+ border-radius: 10px;
1155
+ padding: 10px 12px 10px 18px;
1156
+ border: 1px solid rgba(208, 196, 182, 0.45);
1157
+ background: rgba(255, 255, 255, 0.75);
1158
+ position: relative;
1159
+ box-shadow: 0 2px 6px rgba(31, 26, 23, 0.04);
1160
+ }
1161
+
1162
+ .session-msg.user {
1163
+ border-color: rgba(210, 107, 90, 0.35);
1164
+ background: rgba(210, 107, 90, 0.08);
1165
+ }
1166
+
1167
+ .session-msg::before {
1168
+ content: "";
1169
+ position: absolute;
1170
+ left: 8px;
1171
+ top: 10px;
1172
+ bottom: 10px;
1173
+ width: 3px;
1174
+ border-radius: 999px;
1175
+ background: rgba(139, 118, 104, 0.45);
1176
+ }
1177
+
1178
+ .session-msg.user::before {
1179
+ background: rgba(210, 107, 90, 0.85);
1180
+ }
1181
+
1182
+ .session-msg.assistant::before {
1183
+ background: rgba(90, 139, 106, 0.6);
1184
+ }
1185
+
1186
+ .session-msg-header {
1187
+ display: flex;
1188
+ align-items: flex-start;
1189
+ justify-content: space-between;
1190
+ gap: var(--spacing-xs);
1191
+ margin-bottom: 6px;
1192
+ font-size: var(--font-size-caption);
1193
+ color: var(--color-text-tertiary);
1194
+ }
1195
+
1196
+ .session-msg-meta {
1197
+ min-width: 0;
1198
+ flex: 1;
1199
+ display: flex;
1200
+ align-items: center;
1201
+ gap: 8px;
1202
+ }
1203
+
1204
+ .session-msg-role {
1205
+ font-weight: var(--font-weight-secondary);
1206
+ color: var(--color-text-secondary);
1207
+ }
1208
+
1209
+ .session-msg-time {
1210
+ white-space: nowrap;
1211
+ overflow: hidden;
1212
+ text-overflow: ellipsis;
1213
+ color: var(--color-text-tertiary);
1214
+ }
1215
+
1216
+ .session-msg-content {
1217
+ font-size: var(--font-size-secondary);
1218
+ line-height: 1.55;
1219
+ color: var(--color-text-primary);
1220
+ white-space: pre-wrap;
1221
+ word-break: break-word;
1222
+ }
1223
+
1224
+ .session-preview-empty {
1225
+ flex: 1;
1226
+ display: flex;
1227
+ align-items: center;
1228
+ justify-content: center;
1229
+ color: var(--color-text-tertiary);
1230
+ font-size: var(--font-size-secondary);
1231
+ padding: var(--spacing-md);
1232
+ text-align: center;
1233
+ flex-direction: column;
1234
+ gap: 8px;
1235
+ }
1236
+
1237
+ .session-preview-empty::before {
1238
+ content: "";
1239
+ width: 34px;
1240
+ height: 34px;
1241
+ border-radius: 50%;
1242
+ background: rgba(210, 107, 90, 0.12);
1243
+ box-shadow: inset 0 0 0 6px rgba(255, 255, 255, 0.7);
1244
+ }
1245
+
1246
+ @media (max-width: 900px) {
1247
+ .session-layout {
1248
+ grid-template-columns: 1fr;
1249
+ height: auto;
1250
+ min-height: 0;
1251
+ }
1252
+
1253
+ .session-toolbar {
1254
+ grid-template-columns: 1fr;
1255
+ }
1256
+
1257
+ .session-toolbar-actions {
1258
+ justify-content: flex-start;
1259
+ }
1260
+
1261
+ .session-list {
1262
+ position: static;
1263
+ max-height: 300px;
1264
+ height: auto;
1265
+ }
1266
+
1267
+ .session-preview {
1268
+ min-height: 360px;
1269
+ max-height: none;
1270
+ height: auto;
1271
+ }
1272
+
1273
+ .session-preview-header {
1274
+ flex-direction: column;
1275
+ align-items: stretch;
1276
+ }
1277
+
1278
+ .session-actions {
1279
+ justify-content: flex-start;
1280
+ }
1281
+ }
1282
+
1283
+ .btn[disabled] {
1284
+ opacity: 0.5;
1285
+ cursor: not-allowed;
1286
+ transform: none;
1287
+ box-shadow: none;
1288
+ }
1289
+
1290
+ /* ============================================
1291
+ 模态框
1292
+ ============================================ */
1293
+ .modal-overlay {
1294
+ position: fixed;
1295
+ top: 0;
1296
+ left: 0;
1297
+ right: 0;
1298
+ bottom: 0;
1299
+ background: linear-gradient(to bottom, rgba(31, 26, 23, 0.3) 0%, rgba(31, 26, 23, 0.5) 100%);
1300
+ display: flex;
1301
+ justify-content: center;
1302
+ align-items: center;
1303
+ z-index: 100;
1304
+ backdrop-filter: blur(8px) saturate(180%);
1305
+ -webkit-backdrop-filter: blur(8px) saturate(180%);
1306
+ animation: fadeIn var(--transition-normal) var(--ease-out-expo);
1307
+ }
1308
+
1309
+ .modal {
1310
+ background: linear-gradient(to bottom, var(--color-surface) 0%, rgba(255, 255, 255, 0.98) 100%);
1311
+ width: 90%;
1312
+ max-width: 400px;
1313
+ border-radius: var(--radius-lg);
1314
+ padding: var(--spacing-md);
1315
+ box-shadow: var(--shadow-modal);
1316
+ border: 1px solid rgba(255, 255, 255, 0.8);
1317
+ animation: slideUp var(--transition-slow) var(--ease-spring);
1318
+ }
1319
+
1320
+ .modal-wide {
1321
+ max-width: 980px;
1322
+ }
1323
+
1324
+ .modal-title {
1325
+ font-size: var(--font-size-title);
1326
+ font-weight: var(--font-weight-title);
1327
+ margin-bottom: var(--spacing-md);
1328
+ color: var(--color-text-primary);
1329
+ letter-spacing: -0.01em;
1330
+ }
1331
+
1332
+ .form-group {
1333
+ margin-bottom: var(--spacing-sm);
1334
+ }
1335
+
1336
+ .form-label {
1337
+ display: block;
1338
+ font-size: var(--font-size-secondary);
1339
+ font-weight: var(--font-weight-secondary);
1340
+ color: var(--color-text-secondary);
1341
+ margin-bottom: 7px;
1342
+ letter-spacing: 0.01em;
1343
+ }
1344
+
1345
+ .form-input {
1346
+ width: 100%;
1347
+ padding: 13px var(--spacing-sm);
1348
+ border: 1.5px solid var(--color-border-soft);
1349
+ border-radius: var(--radius-sm);
1350
+ font-size: var(--font-size-body);
1351
+ background-color: var(--color-surface-alt);
1352
+ color: var(--color-text-primary);
1353
+ outline: none;
1354
+ transition: all var(--transition-fast) var(--ease-spring);
1355
+ font-family: var(--font-family-body);
1356
+ box-shadow: inset 0 1px 2px rgba(31, 26, 23, 0.04);
1357
+ }
1358
+
1359
+ .form-input:hover {
1360
+ border-color: var(--color-border-strong);
1361
+ }
1362
+
1363
+ .form-input:focus {
1364
+ border-color: var(--color-brand);
1365
+ background-color: var(--color-surface);
1366
+ box-shadow: var(--shadow-input-focus);
1367
+ }
1368
+
1369
+ .form-input::placeholder {
1370
+ color: var(--color-text-tertiary);
1371
+ opacity: 0.7;
1372
+ }
1373
+
1374
+ .template-editor {
1375
+ min-height: min(60vh, 520px);
1376
+ max-height: min(65vh, 620px);
1377
+ resize: vertical;
1378
+ overflow: auto;
1379
+ white-space: pre;
1380
+ font-family: var(--font-family-mono);
1381
+ font-size: 13px;
1382
+ line-height: 1.45;
1383
+ }
1384
+
1385
+ .template-editor-warning {
1386
+ margin-top: 8px;
1387
+ color: #8d5b31;
1388
+ font-size: var(--font-size-caption);
1389
+ line-height: 1.4;
1390
+ }
1391
+
1392
+ .form-input:disabled,
1393
+ .form-input[readonly] {
1394
+ background: linear-gradient(to right, var(--color-bg) 0%, rgba(247, 241, 232, 0.5) 100%);
1395
+ color: var(--color-text-tertiary);
1396
+ cursor: not-allowed;
1397
+ border-color: transparent;
1398
+ }
1399
+
1400
+ .form-hint {
1401
+ font-size: var(--font-size-caption);
1402
+ color: var(--color-text-tertiary);
1403
+ margin-top: 5px;
1404
+ opacity: 0.8;
1405
+ }
1406
+
1407
+ .btn-group {
1408
+ display: flex;
1409
+ gap: var(--spacing-sm);
1410
+ margin-top: var(--spacing-md);
1411
+ }
1412
+
1413
+ .btn {
1414
+ flex: 1;
1415
+ padding: 14px var(--spacing-sm);
1416
+ border-radius: var(--radius-sm);
1417
+ font-size: var(--font-size-body);
1418
+ font-weight: var(--font-weight-secondary);
1419
+ cursor: pointer;
1420
+ transition: all var(--transition-fast) var(--ease-spring);
1421
+ border: 1px solid var(--color-border-soft);
1422
+ background: linear-gradient(to bottom, var(--color-surface) 0%, rgba(255, 255, 255, 0.95) 100%);
1423
+ color: var(--color-text-secondary);
1424
+ box-shadow: var(--shadow-subtle);
1425
+ letter-spacing: -0.01em;
1426
+ }
1427
+
1428
+ .btn:active {
1429
+ transform: scale(0.97);
1430
+ }
1431
+
1432
+ .btn-cancel {
1433
+ background: linear-gradient(to bottom, var(--color-bg) 0%, rgba(247, 241, 232, 0.8) 100%);
1434
+ color: var(--color-text-primary);
1435
+ border: 1px solid var(--color-border-soft);
1436
+ }
1437
+
1438
+ .btn-cancel:hover {
1439
+ background: linear-gradient(to bottom, var(--color-border) 0%, rgba(208, 196, 182, 0.5) 100%);
1440
+ }
1441
+
1442
+ .btn-confirm {
1443
+ background: linear-gradient(135deg, var(--color-brand) 0%, var(--color-brand-dark) 100%);
1444
+ color: white;
1445
+ box-shadow: 0 2px 4px rgba(210, 107, 90, 0.2);
1446
+ border: none;
1447
+ }
1448
+
1449
+ .btn-confirm:hover {
1450
+ box-shadow: 0 4px 8px rgba(210, 107, 90, 0.25);
1451
+ filter: brightness(1.05);
1452
+ }
1453
+
1454
+ .btn-confirm.secondary {
1455
+ background: linear-gradient(135deg, var(--color-success) 0%, rgba(90, 139, 106, 0.85) 100%);
1456
+ box-shadow: 0 2px 4px rgba(90, 139, 106, 0.2);
1457
+ border: none;
1458
+ }
1459
+
1460
+ .btn-confirm.secondary:hover {
1461
+ box-shadow: 0 4px 8px rgba(90, 139, 106, 0.25);
1462
+ filter: brightness(1.05);
1463
+ }
1464
+
1465
+ /* ============================================
1466
+ 模型列表
1467
+ ============================================ */
1468
+ .model-list {
1469
+ max-height: 200px;
1470
+ overflow-y: auto;
1471
+ border: 1px solid rgba(208, 196, 182, 0.4);
1472
+ border-radius: var(--radius-sm);
1473
+ margin-bottom: var(--spacing-sm);
1474
+ scrollbar-width: none;
1475
+ background: linear-gradient(to bottom, var(--color-surface) 0%, rgba(255, 255, 255, 0.8) 100%);
1476
+ box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.02);
1477
+ }
1478
+
1479
+ .model-list::-webkit-scrollbar {
1480
+ display: none;
1481
+ }
1482
+
1483
+ .model-item {
1484
+ display: flex;
1485
+ justify-content: space-between;
1486
+ align-items: center;
1487
+ padding: 11px var(--spacing-sm);
1488
+ border-bottom: 1px solid rgba(208, 196, 182, 0.3);
1489
+ font-size: var(--font-size-body);
1490
+ color: var(--color-text-primary);
1491
+ transition: all var(--transition-fast) var(--ease-spring);
1492
+ letter-spacing: -0.005em;
1493
+ }
1494
+
1495
+ .model-item:last-child {
1496
+ border-bottom: none;
1497
+ }
1498
+
1499
+ .model-item:hover {
1500
+ background: linear-gradient(to right, rgba(247, 241, 232, 0.6) 0%, rgba(247, 241, 232, 0.3) 100%);
1501
+ }
1502
+
1503
+ .btn-remove-model {
1504
+ font-size: var(--font-size-caption);
1505
+ font-weight: var(--font-weight-caption);
1506
+ color: var(--color-text-tertiary);
1507
+ cursor: pointer;
1508
+ padding: 5px 10px;
1509
+ border-radius: var(--radius-full);
1510
+ transition: all var(--transition-fast) var(--ease-spring);
1511
+ background: transparent;
1512
+ border: 1px solid rgba(139, 118, 104, 0.2);
1513
+ letter-spacing: 0.03em;
1514
+ }
1515
+
1516
+ .btn-remove-model:hover {
1517
+ background: linear-gradient(135deg, var(--color-error) 0%, rgba(200, 74, 58, 0.9) 100%);
1518
+ color: white;
1519
+ transform: scale(1.08);
1520
+ box-shadow: 0 2px 6px rgba(200, 74, 58, 0.25);
1521
+ border-color: transparent;
1522
+ }
1523
+
1524
+ /* ============================================
1525
+ Toast - 顶部横幅
1526
+ ============================================ */
1527
+ .toast {
1528
+ position: fixed;
1529
+ top: 16px;
1530
+ left: 50%;
1531
+ transform: translateX(-50%);
1532
+ background: linear-gradient(to bottom, var(--color-surface) 0%, rgba(255, 255, 255, 0.95) 100%);
1533
+ padding: 12px 24px;
1534
+ border-radius: var(--radius-full);
1535
+ box-shadow: var(--shadow-raised);
1536
+ z-index: 200;
1537
+ animation: slideDown var(--transition-slow) var(--ease-spring);
1538
+ display: flex;
1539
+ align-items: center;
1540
+ gap: var(--spacing-xs);
1541
+ font-size: var(--font-size-body);
1542
+ font-weight: var(--font-weight-body);
1543
+ border: 1px solid rgba(255, 255, 255, 0.8);
1544
+ backdrop-filter: blur(8px);
1545
+ -webkit-backdrop-filter: blur(8px);
1546
+ }
1547
+
1548
+ .toast.error {
1549
+ border-left: 3px solid var(--color-error);
1550
+ }
1551
+
1552
+ .toast.success {
1553
+ border-left: 3px solid var(--color-success);
1554
+ }
1555
+
1556
+ /* ============================================
1557
+ 状态消息
1558
+ ============================================ */
1559
+ .state-message {
1560
+ text-align: center;
1561
+ padding: 60px var(--spacing-md);
1562
+ color: var(--color-text-tertiary);
1563
+ font-size: var(--font-size-body);
1564
+ opacity: 0.7;
1565
+ letter-spacing: -0.005em;
1566
+ }
1567
+
1568
+ .state-message.error {
1569
+ color: var(--color-error);
1570
+ }
1571
+
1572
+ /* 空状态 */
1573
+ .empty-state {
1574
+ text-align: center;
1575
+ padding: 48px var(--spacing-md);
1576
+ color: var(--color-text-tertiary);
1577
+ }
1578
+
1579
+ .empty-state-icon {
1580
+ width: 48px;
1581
+ height: 48px;
1582
+ margin: 0 auto var(--spacing-sm);
1583
+ opacity: 0.3;
1584
+ }
1585
+
1586
+ .empty-state-title {
1587
+ font-size: var(--font-size-body);
1588
+ font-weight: var(--font-weight-secondary);
1589
+ color: var(--color-text-secondary);
1590
+ margin-bottom: 4px;
1591
+ }
1592
+
1593
+ .empty-state-subtitle {
1594
+ font-size: var(--font-size-secondary);
1595
+ color: var(--color-text-tertiary);
1596
+ opacity: 0.8;
1597
+ }
1598
+
1599
+ /* ============================================
1600
+ 动画
1601
+ ============================================ */
1602
+ @keyframes fadeIn {
1603
+ from { opacity: 0; }
1604
+ to { opacity: 1; }
1605
+ }
1606
+
1607
+ @keyframes slideUp {
1608
+ from { transform: translateY(24px); opacity: 0; }
1609
+ to { transform: translateY(0); opacity: 1; }
1610
+ }
1611
+
1612
+ @keyframes slideDown {
1613
+ from { transform: translateX(-50%) translateY(-100%); opacity: 0; }
1614
+ to { transform: translateX(-50%) translateY(0); opacity: 1; }
1615
+ }
1616
+
1617
+ @keyframes spin {
1618
+ from { transform: rotate(0deg); }
1619
+ to { transform: rotate(360deg); }
1620
+ }
1621
+
1622
+ [v-cloak] {
1623
+ display: none !important;
1624
+ }
1625
+
1626
+ /* 模式内容容器 */
1627
+ .mode-content {
1628
+ animation: fadeIn var(--transition-normal) var(--ease-spring);
1629
+ }
1630
+
1631
+ /* 内容区域包裹器 - 稳定高度 */
1632
+ .content-wrapper {
1633
+ min-height: 300px;
1634
+ position: relative;
1635
+ }
1636
+
1637
+ button:focus-visible,
1638
+ select:focus-visible,
1639
+ input:focus-visible,
1640
+ textarea:focus-visible {
1641
+ outline: 3px solid rgba(201, 94, 75, 0.25);
1642
+ outline-offset: 2px;
1643
+ }
1644
+
1645
+ @media (max-width: 720px) {
1646
+ .main-title {
1647
+ font-size: 40px;
1648
+ }
1649
+
1650
+ .subtitle {
1651
+ font-size: var(--font-size-secondary);
1652
+ margin-bottom: 16px;
1653
+ }
1654
+
1655
+ .segmented-control {
1656
+ flex-direction: column;
1657
+ gap: 6px;
1658
+ }
1659
+ }
1660
+ </style>
1661
+ </head>
1662
+ <body>
1663
+ <div id="app" class="container" v-cloak>
1664
+ <!-- 主标题 -->
1665
+ <h1 class="main-title">
1666
+ Codex<br>
1667
+ <span class="accent">Mate.</span>
1668
+ </h1>
1669
+ <p class="subtitle">本地配置中枢,统一管理 Codex / Claude Code / 会话。</p>
1670
+
1671
+ <!-- 模式切换器 -->
1672
+ <div class="segmented-control">
1673
+ <button :class="['segment', { active: configMode === 'codex' }]" @click="switchConfigMode('codex')">
1674
+ Codex 配置
1675
+ </button>
1676
+ <button :class="['segment', { active: configMode === 'claude' }]" @click="switchConfigMode('claude')">
1677
+ Claude Code 配置
1678
+ </button>
1679
+ <button :class="['segment', { active: configMode === 'sessions' }]" @click="switchConfigMode('sessions')">
1680
+ 会话浏览
1681
+ </button>
1682
+ </div>
1683
+
1684
+ <!-- 内容包裹器 - 稳定布局 -->
1685
+ <div class="content-wrapper">
1686
+ <!-- Codex 配置模式 -->
1687
+ <div v-show="configMode === 'codex'" class="mode-content">
1688
+ <!-- 添加提供商按钮 -->
1689
+ <button class="btn-add" @click="showAddModal = true" v-if="!loading && !initError">
1690
+ <svg class="icon" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2">
1691
+ <path d="M10 4v12M4 10h12"/>
1692
+ </svg>
1693
+ 添加提供商
1694
+ </button>
1695
+
1696
+ <!-- 模型选择器 -->
1697
+ <div class="selector-section">
1698
+ <div class="selector-header">
1699
+ <span class="selector-title">模型</span>
1700
+ <div class="selector-actions">
1701
+ <button class="btn-icon" @click="showModelModal = true" title="添加模型">+</button>
1702
+ <button class="btn-icon" @click="showModelListModal = true" title="管理模型">≡</button>
1703
+ </div>
1704
+ </div>
1705
+ <select class="model-select" v-model="currentModel" @change="onModelChange">
1706
+ <option v-for="model in models" :key="model" :value="model">{{ model }}</option>
1707
+ </select>
1708
+ <div class="config-template-hint">
1709
+ Codex 配置改动采用模板确认模式:请先编辑模板,再手动确认应用。
1710
+ </div>
1711
+ <button class="btn-tool btn-template-editor" @click="openConfigTemplateEditor" :disabled="loading || !!initError">
1712
+ 打开 Config 模板编辑器
1713
+ </button>
1714
+ </div>
1715
+
1716
+ <div class="selector-section">
1717
+ <div class="selector-header">
1718
+ <span class="selector-title">AGENTS.md</span>
1719
+ </div>
1720
+ <div class="config-template-hint">
1721
+ 管理 Codex 指令文件,默认读写 <code>~/.codex/AGENTS.md</code>(与 <code>config.toml</code> 同级)。
1722
+ </div>
1723
+ <button class="btn-tool" @click="openAgentsEditor" :disabled="loading || !!initError || agentsLoading">
1724
+ {{ agentsLoading ? '加载中...' : '打开 AGENTS.md 编辑器' }}
1725
+ </button>
1726
+ </div>
1727
+
1728
+ <div v-if="!loading && !initError" class="card-list">
1729
+ <div v-for="provider in providersList" :key="provider.name"
1730
+ :class="['card', { active: currentProvider === provider.name }]"
1731
+ @click="switchProvider(provider.name)">
1732
+ <div class="card-leading">
1733
+ <div class="card-icon">{{ provider.name.charAt(0).toUpperCase() }}</div>
1734
+ <div class="card-content">
1735
+ <div class="card-title">{{ provider.name }}</div>
1736
+ <div class="card-subtitle">{{ provider.url || '未设置 URL' }}</div>
1737
+ </div>
1738
+ </div>
1739
+ <div class="card-trailing">
1740
+ <span :class="['pill', provider.hasKey ? 'configured' : 'empty']">
1741
+ {{ provider.hasKey ? '已配置' : '未配置' }}
1742
+ </span>
1743
+ <span v-if="speedResults[provider.name]" :class="['latency', speedResults[provider.name].ok ? 'ok' : 'error']">
1744
+ {{ formatLatency(speedResults[provider.name]) }}
1745
+ </span>
1746
+ <div class="card-actions" @click.stop>
1747
+ <button class="card-action-btn" :class="{ loading: speedLoading[provider.name] }" @click="runSpeedTest(provider.name)" title="Speed Test">
1748
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1749
+ <path d="M13 2L3 14h7l-1 8 12-14h-7l-1-6z"/>
1750
+ </svg>
1751
+ </button>
1752
+ <button class="card-action-btn" @click="openEditModal(provider)" title="编辑">
1753
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1754
+ <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
1755
+ <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
1756
+ </svg>
1757
+ </button>
1758
+ <button class="card-action-btn delete" @click="deleteProvider(provider.name)" title="删除">
1759
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1760
+ <path d="M3 6h18"/>
1761
+ <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
1762
+ </svg>
1763
+ </button>
1764
+ </div>
1765
+ </div>
1766
+ </div>
1767
+ </div>
1768
+ </div>
1769
+
1770
+ <!-- Claude Code 配置模式 -->
1771
+ <div v-show="configMode === 'claude'" class="mode-content">
1772
+ <!-- 添加提供商按钮 -->
1773
+ <button class="btn-add" @click="showClaudeConfigModal = true" v-if="!loading && !initError">
1774
+ <svg class="icon" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2">
1775
+ <path d="M10 4v12M4 10h12"/>
1776
+ </svg>
1777
+ 添加提供商
1778
+ </button>
1779
+ <div class="config-template-hint">
1780
+ 默认应用到 <code>~/.claude/settings.json</code>;编辑弹窗中可使用兼容模式写入系统环境变量。
1781
+ </div>
1782
+
1783
+ <div class="card-list">
1784
+ <div v-for="(config, name) in claudeConfigs" :key="name"
1785
+ :class="['card', { active: currentClaudeConfig === name }]"
1786
+ @click="applyClaudeConfig(name)">
1787
+ <div class="card-leading">
1788
+ <div class="card-icon">{{ name.charAt(0).toUpperCase() }}</div>
1789
+ <div class="card-content">
1790
+ <div class="card-title">{{ name }}</div>
1791
+ <div class="card-subtitle">{{ config.model || '未设置模型' }}</div>
1792
+ </div>
1793
+ </div>
1794
+ <div class="card-trailing">
1795
+ <span :class="['pill', config.hasKey ? 'configured' : 'empty']">
1796
+ {{ config.hasKey ? '已配置' : '未配置' }}
1797
+ </span>
1798
+ <div class="card-actions" @click.stop>
1799
+ <button class="card-action-btn" @click="openEditConfigModal(name)" title="编辑">
1800
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1801
+ <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/>
1802
+ <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
1803
+ </svg>
1804
+ </button>
1805
+ <button class="card-action-btn delete" @click="deleteClaudeConfig(name)" title="删除">
1806
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1807
+ <path d="M3 6h18"/>
1808
+ <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/>
1809
+ </svg>
1810
+ </button>
1811
+ </div>
1812
+ </div>
1813
+ </div>
1814
+ </div>
1815
+ </div>
1816
+
1817
+ <!-- 会话浏览模式 -->
1818
+ <div v-show="configMode === 'sessions'" class="mode-content">
1819
+ <div class="selector-section">
1820
+ <div class="selector-header">
1821
+ <span class="selector-title">会话来源</span>
1822
+ </div>
1823
+ <div class="session-toolbar">
1824
+ <div class="session-toolbar-group">
1825
+ <select class="session-source-select" v-model="sessionFilterSource" @change="onSessionSourceChange" :disabled="sessionsLoading">
1826
+ <option value="all">全部(Codex + Claude)</option>
1827
+ <option value="codex">仅 Codex</option>
1828
+ <option value="claude">仅 Claude Code</option>
1829
+ </select>
1830
+ <select
1831
+ class="session-path-select"
1832
+ v-model="sessionPathFilter"
1833
+ @change="onSessionPathFilterChange"
1834
+ :disabled="sessionsLoading">
1835
+ <option value="">全部路径</option>
1836
+ <option v-for="cwd in sessionPathOptions" :key="cwd" :value="cwd">{{ cwd }}</option>
1837
+ </select>
1838
+ </div>
1839
+ <div class="session-toolbar-group session-toolbar-grow">
1840
+ <input
1841
+ class="session-query-input"
1842
+ v-model="sessionQuery"
1843
+ @keyup.enter="loadSessions"
1844
+ disabled
1845
+ placeholder="关键词检索(暂时停用)">
1846
+ </div>
1847
+ <div class="session-toolbar-group">
1848
+ <select
1849
+ class="session-role-select"
1850
+ v-model="sessionRoleFilter"
1851
+ @change="onSessionFilterChange"
1852
+ disabled>
1853
+ <option value="all">全部角色</option>
1854
+ <option value="user">仅 User</option>
1855
+ <option value="assistant">仅 Assistant</option>
1856
+ <option value="system">仅 System</option>
1857
+ </select>
1858
+ <select
1859
+ class="session-time-select"
1860
+ v-model="sessionTimePreset"
1861
+ @change="onSessionFilterChange"
1862
+ disabled>
1863
+ <option value="all">全部时间</option>
1864
+ <option value="7d">近 7 天</option>
1865
+ <option value="30d">近 30 天</option>
1866
+ <option value="90d">近 90 天</option>
1867
+ </select>
1868
+ </div>
1869
+ <div class="session-toolbar-group session-toolbar-actions">
1870
+ <button class="btn-tool" @click="loadSessions" :disabled="sessionsLoading">
1871
+ {{ sessionsLoading ? '刷新中...' : '刷新会话' }}
1872
+ </button>
1873
+ <button class="btn-tool" @click="clearSessionFilters" :disabled="sessionsLoading">
1874
+ 清空筛选
1875
+ </button>
1876
+ </div>
1877
+ </div>
1878
+ <div class="session-hint">
1879
+ 关键词检索与角色/时间筛选暂时停用;当前仅支持来源与路径筛选会话列表。右侧预览区仅支持查看与导出。
1880
+ </div>
1881
+ </div>
1882
+
1883
+ <div v-if="sessionsLoading" class="state-message">
1884
+ 会话加载中...
1885
+ </div>
1886
+
1887
+ <div v-else-if="sessionsList.length === 0" class="session-empty">
1888
+ 暂无可用会话记录
1889
+ </div>
1890
+
1891
+ <div v-else class="session-layout">
1892
+ <div class="session-list">
1893
+ <div
1894
+ v-for="session in sessionsList"
1895
+ :key="session.source + '-' + session.sessionId + '-' + session.filePath"
1896
+ :class="[
1897
+ 'session-item',
1898
+ {
1899
+ active: activeSession && getSessionExportKey(activeSession) === getSessionExportKey(session)
1900
+ }
1901
+ ]"
1902
+ @click="selectSession(session)">
1903
+ <div class="session-item-header">
1904
+ <div class="session-item-main">
1905
+ <div class="session-item-title">{{ session.title || session.sessionId }}</div>
1906
+ </div>
1907
+ <div class="session-item-actions">
1908
+ <button
1909
+ v-if="isResumeCommandAvailable(session)"
1910
+ class="session-item-copy"
1911
+ @click.stop="copyResumeCommand(session)"
1912
+ :disabled="sessionsLoading"
1913
+ aria-label="复制恢复命令"
1914
+ title="复制恢复命令">
1915
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
1916
+ <rect x="8" y="8" width="12" height="12" rx="2"></rect>
1917
+ <path d="M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2"></path>
1918
+ </svg>
1919
+ </button>
1920
+ </div>
1921
+ </div>
1922
+ <div class="session-item-meta">
1923
+ <span class="session-source">{{ session.sourceLabel }}</span>
1924
+ <span class="session-item-time">{{ session.updatedAt || 'unknown time' }}</span>
1925
+ <span class="session-count-pill">{{ session.messageCount || 0 }} 条</span>
1926
+ </div>
1927
+ <div v-if="session.match && session.match.hit" class="session-item-sub session-item-snippet">
1928
+ <span v-if="session.match.snippets && session.match.snippets.length">{{ session.match.snippets.join(' · ') }}</span>
1929
+ <span v-else>命中 {{ session.match.count || 1 }} 处</span>
1930
+ </div>
1931
+ </div>
1932
+ </div>
1933
+
1934
+ <div class="session-preview">
1935
+ <template v-if="activeSession">
1936
+ <div class="session-preview-scroll">
1937
+ <div class="session-preview-header">
1938
+ <div>
1939
+ <div class="session-preview-title">{{ activeSession.title || activeSession.sessionId }}</div>
1940
+ <div class="session-preview-meta">
1941
+ <span class="session-preview-meta-item">{{ activeSession.sourceLabel }}</span>
1942
+ <span class="session-preview-meta-item">{{ activeSession.updatedAt || 'unknown time' }}</span>
1943
+ </div>
1944
+ <div class="session-preview-meta" v-if="activeSession.cwd">
1945
+ <span class="session-preview-meta-item">{{ activeSession.cwd }}</span>
1946
+ </div>
1947
+ </div>
1948
+ <div class="session-actions">
1949
+ <button class="btn-session-refresh" @click="loadActiveSessionDetail" :disabled="sessionDetailLoading || !activeSession">
1950
+ {{ sessionDetailLoading ? '加载中...' : '刷新内容' }}
1951
+ </button>
1952
+ <button
1953
+ class="btn-session-export"
1954
+ @click="exportSession(activeSession)"
1955
+ :disabled="!activeSession || sessionExporting[getSessionExportKey(activeSession)]">
1956
+ {{ (activeSession && sessionExporting[getSessionExportKey(activeSession)]) ? '导出中...' : '导出记录' }}
1957
+ </button>
1958
+ </div>
1959
+ </div>
1960
+
1961
+ <div v-if="sessionDetailLoading" class="session-preview-empty">
1962
+ 正在加载会话内容...
1963
+ </div>
1964
+
1965
+ <div v-else-if="activeSessionDetailError" class="session-preview-empty">
1966
+ {{ activeSessionDetailError }}
1967
+ </div>
1968
+
1969
+ <div v-else-if="!activeSessionMessages.length" class="session-preview-empty">
1970
+ 当前会话暂无可展示消息
1971
+ </div>
1972
+
1973
+ <div v-else class="session-preview-body">
1974
+ <div v-if="activeSessionDetailClipped" class="session-item-sub session-item-wrap">
1975
+ 仅展示最近 {{ activeSessionMessages.length }} 条消息。
1976
+ </div>
1977
+ <div
1978
+ v-for="(msg, idx) in activeSessionMessages"
1979
+ :key="getRecordRenderKey(msg, idx)"
1980
+ :class="['session-msg', msg.role === 'user' ? 'user' : (msg.role === 'system' ? 'system' : 'assistant')]">
1981
+ <div class="session-msg-header">
1982
+ <div class="session-msg-meta">
1983
+ <span class="session-msg-role">{{ msg.role === 'user' ? 'User' : (msg.role === 'system' ? 'System' : 'Assistant') }}</span>
1984
+ <span class="session-msg-time">{{ msg.timestamp || '' }}</span>
1985
+ </div>
1986
+ </div>
1987
+ <div class="session-msg-content">{{ msg.text || '' }}</div>
1988
+ </div>
1989
+ </div>
1990
+ </div>
1991
+ </template>
1992
+
1993
+ <div v-else class="session-preview-empty">
1994
+ 请先在左侧选择一个会话
1995
+ </div>
1996
+ </div>
1997
+ </div>
1998
+ </div>
1999
+
2000
+ <!-- 加载状态 -->
2001
+ <div v-if="loading" class="state-message">
2002
+ 加载配置中...
2003
+ </div>
2004
+
2005
+ <!-- 错误状态 -->
2006
+ <div v-else-if="initError" class="state-message error">
2007
+ {{ initError }}
2008
+ </div>
2009
+ </div>
2010
+
2011
+ <!-- 添加提供商模态框 -->
2012
+ <div v-if="showAddModal" class="modal-overlay" @click.self="showAddModal = false">
2013
+ <div class="modal">
2014
+ <div class="modal-title">添加提供商</div>
2015
+
2016
+ <div class="form-group">
2017
+ <label class="form-label">名称</label>
2018
+ <input v-model="newProvider.name" class="form-input" placeholder="例如: myapi">
2019
+ </div>
2020
+ <div class="form-group">
2021
+ <label class="form-label">API 端点</label>
2022
+ <input v-model="newProvider.url" class="form-input" placeholder="https://api.example.com/v1">
2023
+ </div>
2024
+ <div class="form-group">
2025
+ <label class="form-label">认证密钥</label>
2026
+ <input v-model="newProvider.key" class="form-input" placeholder="sk-...">
2027
+ </div>
2028
+
2029
+ <div class="btn-group">
2030
+ <button class="btn btn-cancel" @click="closeAddModal">取消</button>
2031
+ <button class="btn btn-confirm" @click="addProvider">添加</button>
2032
+ </div>
2033
+ </div>
2034
+ </div>
2035
+
2036
+ <!-- 编辑提供商模态框 -->
2037
+ <div v-if="showEditModal" class="modal-overlay" @click.self="closeEditModal">
2038
+ <div class="modal">
2039
+ <div class="modal-title">编辑提供商</div>
2040
+
2041
+ <div class="form-group">
2042
+ <label class="form-label">名称</label>
2043
+ <input v-model="editingProvider.name" class="form-input" placeholder="提供商名称" readonly>
2044
+ </div>
2045
+ <div class="form-group">
2046
+ <label class="form-label">API 端点</label>
2047
+ <input v-model="editingProvider.url" class="form-input" placeholder="https://api.example.com/v1">
2048
+ </div>
2049
+ <div class="form-group">
2050
+ <label class="form-label">认证密钥</label>
2051
+ <input v-model="editingProvider.key" class="form-input" placeholder="留空则保持不变">
2052
+ <div class="form-hint">留空表示不修改密钥</div>
2053
+ </div>
2054
+
2055
+ <div class="btn-group">
2056
+ <button class="btn btn-cancel" @click="closeEditModal">取消</button>
2057
+ <button class="btn btn-confirm" @click="updateProvider">保存</button>
2058
+ </div>
2059
+ </div>
2060
+ </div>
2061
+
2062
+ <!-- 添加模型模态框 -->
2063
+ <div v-if="showModelModal" class="modal-overlay" @click.self="showModelModal = false">
2064
+ <div class="modal">
2065
+ <div class="modal-title">添加模型</div>
2066
+
2067
+ <div class="form-group">
2068
+ <label class="form-label">模型名称</label>
2069
+ <input v-model="newModelName" class="form-input" placeholder="例如: gpt-5">
2070
+ </div>
2071
+
2072
+ <div class="btn-group">
2073
+ <button class="btn btn-cancel" @click="closeModelModal">取消</button>
2074
+ <button class="btn btn-confirm" @click="addModel">添加</button>
2075
+ </div>
2076
+ </div>
2077
+ </div>
2078
+
2079
+ <!-- 模型列表模态框 -->
2080
+ <div v-if="showModelListModal" class="modal-overlay" @click.self="showModelListModal = false">
2081
+ <div class="modal">
2082
+ <div class="modal-title">管理模型</div>
2083
+
2084
+ <div class="model-list">
2085
+ <div v-for="model in models" :key="model" class="model-item">
2086
+ <span>{{ model }}</span>
2087
+ <span class="btn-remove-model" @click="removeModel(model)">删除</span>
2088
+ </div>
2089
+ </div>
2090
+
2091
+ <div class="btn-group">
2092
+ <button class="btn btn-confirm" @click="showModelListModal = false">关闭</button>
2093
+ </div>
2094
+ </div>
2095
+ </div>
2096
+
2097
+ <!-- 添加Claude配置模态框 -->
2098
+ <div v-if="showClaudeConfigModal" class="modal-overlay" @click.self="showClaudeConfigModal = false">
2099
+ <div class="modal">
2100
+ <div class="modal-title">添加 Claude Code 配置</div>
2101
+
2102
+ <div class="form-group">
2103
+ <label class="form-label">配置名称</label>
2104
+ <input v-model="newClaudeConfig.name" class="form-input" placeholder="例如: 智谱GLM">
2105
+ </div>
2106
+ <div class="form-group">
2107
+ <label class="form-label">API Key</label>
2108
+ <input v-model="newClaudeConfig.apiKey" class="form-input" placeholder="sk-ant-...">
2109
+ </div>
2110
+ <div class="form-group">
2111
+ <label class="form-label">Base URL</label>
2112
+ <input v-model="newClaudeConfig.baseUrl" class="form-input" placeholder="https://open.bigmodel.cn/api/anthropic">
2113
+ </div>
2114
+ <div class="form-group">
2115
+ <label class="form-label">模型</label>
2116
+ <input v-model="newClaudeConfig.model" class="form-input" placeholder="例如: claude-sonnet-4-20250514">
2117
+ </div>
2118
+
2119
+ <div class="btn-group">
2120
+ <button class="btn btn-cancel" @click="closeClaudeConfigModal">取消</button>
2121
+ <button class="btn btn-confirm" @click="addClaudeConfig">添加</button>
2122
+ </div>
2123
+ </div>
2124
+ </div>
2125
+
2126
+ <!-- 编辑Claude配置模态框 -->
2127
+ <div v-if="showEditConfigModal" class="modal-overlay" @click.self="closeEditConfigModal">
2128
+ <div class="modal">
2129
+ <div class="modal-title">编辑 Claude Code 配置</div>
2130
+
2131
+ <div class="form-group">
2132
+ <label class="form-label">配置名称</label>
2133
+ <input v-model="editingConfig.name" class="form-input" placeholder="配置名称" readonly>
2134
+ </div>
2135
+ <div class="form-group">
2136
+ <label class="form-label">API Key</label>
2137
+ <input v-model="editingConfig.apiKey" class="form-input" placeholder="sk-ant-...">
2138
+ </div>
2139
+ <div class="form-group">
2140
+ <label class="form-label">Base URL</label>
2141
+ <input v-model="editingConfig.baseUrl" class="form-input" placeholder="https://open.bigmodel.cn/api/anthropic">
2142
+ </div>
2143
+ <div class="form-group">
2144
+ <label class="form-label">模型</label>
2145
+ <input v-model="editingConfig.model" class="form-input" placeholder="例如: claude-sonnet-4-20250514">
2146
+ </div>
2147
+
2148
+ <div class="btn-group">
2149
+ <button class="btn btn-cancel" @click="closeEditConfigModal">取消</button>
2150
+ <button class="btn btn-confirm" @click="updateConfig">保存</button>
2151
+ <button class="btn btn-confirm secondary" @click="saveAndApplyConfig">保存并应用到 Claude 配置</button>
2152
+ <button class="btn btn-confirm secondary" @click="saveAndApplyEnvCompat">兼容模式应用到环境变量</button>
2153
+ </div>
2154
+ </div>
2155
+ </div>
2156
+
2157
+ <div v-if="showConfigTemplateModal" class="modal-overlay" @click.self="closeConfigTemplateModal">
2158
+ <div class="modal modal-wide">
2159
+ <div class="modal-title">Config 模板编辑器(手动确认应用)</div>
2160
+
2161
+ <div class="form-group">
2162
+ <label class="form-label">config.toml 模板</label>
2163
+ <textarea
2164
+ v-model="configTemplateContent"
2165
+ class="form-input template-editor"
2166
+ spellcheck="false"
2167
+ placeholder="在这里编辑 config.toml 模板内容"></textarea>
2168
+ <div class="template-editor-warning">
2169
+ 工具不会自动改动 `config.toml`;只有点击“确认应用模板”后才写入。
2170
+ </div>
2171
+ </div>
2172
+
2173
+ <div class="btn-group">
2174
+ <button class="btn btn-cancel" @click="closeConfigTemplateModal">取消</button>
2175
+ <button class="btn btn-confirm" @click="applyConfigTemplate" :disabled="configTemplateApplying">
2176
+ {{ configTemplateApplying ? '应用中...' : '确认应用模板' }}
2177
+ </button>
2178
+ </div>
2179
+ </div>
2180
+ </div>
2181
+
2182
+ <div v-if="showAgentsModal" class="modal-overlay" @click.self="closeAgentsModal">
2183
+ <div class="modal modal-wide">
2184
+ <div class="modal-title">AGENTS.md 编辑器</div>
2185
+
2186
+ <div class="form-group">
2187
+ <label class="form-label">目标文件</label>
2188
+ <div class="form-hint">
2189
+ {{ agentsPath || '未加载' }}
2190
+ <span v-if="agentsPath">
2191
+ ({{ agentsExists ? '已存在' : '不存在,将在保存时创建' }})
2192
+ </span>
2193
+ </div>
2194
+ </div>
2195
+
2196
+ <div class="form-group">
2197
+ <label class="form-label">AGENTS.md 内容</label>
2198
+ <textarea
2199
+ v-model="agentsContent"
2200
+ class="form-input template-editor"
2201
+ spellcheck="false"
2202
+ :readonly="agentsLoading"
2203
+ placeholder="在这里编辑 AGENTS.md 内容"></textarea>
2204
+ <div class="template-editor-warning">
2205
+ 保存后会写入目标 AGENTS.md(与 config.toml 同级)。
2206
+ </div>
2207
+ </div>
2208
+
2209
+ <div class="btn-group">
2210
+ <button class="btn btn-cancel" @click="closeAgentsModal">取消</button>
2211
+ <button class="btn btn-confirm" @click="applyAgentsContent" :disabled="agentsSaving || agentsLoading">
2212
+ {{ agentsSaving ? '保存中...' : '保存' }}
2213
+ </button>
2214
+ </div>
2215
+ </div>
2216
+ </div>
2217
+
2218
+ <!-- Toast通知 -->
2219
+
2220
+ <!-- Toast -->
2221
+ <div v-if="message" :class="['toast', messageType]">{{ message }}</div>
2222
+ </div>
2223
+
2224
+ <script>
2225
+ const { createApp } = Vue;
2226
+ const API_BASE = 'http://localhost:3737';
2227
+
2228
+ async function api(action, params = {}) {
2229
+ const res = await fetch(`${API_BASE}/api`, {
2230
+ method: 'POST',
2231
+ headers: { 'Content-Type': 'application/json' },
2232
+ body: JSON.stringify({ action, params })
2233
+ });
2234
+ return await res.json();
2235
+ }
2236
+
2237
+ const app = createApp({
2238
+ data() {
2239
+ return {
2240
+ configMode: 'codex',
2241
+ currentProvider: '',
2242
+ currentModel: '',
2243
+ providersList: [],
2244
+ models: [],
2245
+ loading: true,
2246
+ initError: '',
2247
+ message: '',
2248
+ messageType: '',
2249
+ showAddModal: false,
2250
+ showEditModal: false,
2251
+ showModelModal: false,
2252
+ showModelListModal: false,
2253
+ showClaudeConfigModal: false,
2254
+ showEditConfigModal: false,
2255
+ showConfigTemplateModal: false,
2256
+ showAgentsModal: false,
2257
+ configTemplateContent: '',
2258
+ configTemplateApplying: false,
2259
+ agentsContent: '',
2260
+ agentsPath: '',
2261
+ agentsExists: false,
2262
+ agentsLineEnding: '\n',
2263
+ agentsLoading: false,
2264
+ agentsSaving: false,
2265
+ sessionsList: [],
2266
+ sessionsLoading: false,
2267
+ sessionFilterSource: 'all',
2268
+ sessionPathFilter: '',
2269
+ sessionQuery: '',
2270
+ sessionRoleFilter: 'all',
2271
+ sessionTimePreset: 'all',
2272
+ sessionPathOptions: [],
2273
+ sessionPathOptionsLoading: false,
2274
+ sessionPathOptionsMap: {
2275
+ all: [],
2276
+ codex: [],
2277
+ claude: []
2278
+ },
2279
+ sessionPathOptionsLoadedMap: {
2280
+ all: false,
2281
+ codex: false,
2282
+ claude: false
2283
+ },
2284
+ sessionPathRequestSeq: 0,
2285
+ sessionExporting: {},
2286
+ activeSession: null,
2287
+ activeSessionMessages: [],
2288
+ activeSessionDetailError: '',
2289
+ activeSessionDetailClipped: false,
2290
+ sessionDetailLoading: false,
2291
+ sessionDetailRequestSeq: 0,
2292
+ speedResults: {},
2293
+ speedLoading: {},
2294
+ newProvider: { name: '', url: '', key: '' },
2295
+ editingProvider: { name: '', url: '', key: '' },
2296
+ newModelName: '',
2297
+ currentClaudeConfig: '',
2298
+ editingConfig: { name: '', apiKey: '', baseUrl: '', model: '' },
2299
+ claudeConfigs: {
2300
+ '智谱GLM': {
2301
+ apiKey: '',
2302
+ baseUrl: 'https://open.bigmodel.cn/api/anthropic',
2303
+ model: 'glm-4.7',
2304
+ hasKey: false
2305
+ }
2306
+ },
2307
+ newClaudeConfig: {
2308
+ name: '',
2309
+ apiKey: '',
2310
+ baseUrl: 'https://open.bigmodel.cn/api/anthropic',
2311
+ model: 'glm-4.7'
2312
+ }
2313
+ }
2314
+ },
2315
+ mounted() {
2316
+ const savedConfigs = localStorage.getItem('claudeConfigs');
2317
+ if (savedConfigs) {
2318
+ try {
2319
+ this.claudeConfigs = JSON.parse(savedConfigs);
2320
+ for (const [name, config] of Object.entries(this.claudeConfigs)) {
2321
+ if (config.apiKey && config.apiKey.includes('****')) {
2322
+ config.apiKey = '';
2323
+ config.hasKey = false;
2324
+ }
2325
+ }
2326
+ localStorage.setItem('claudeConfigs', JSON.stringify(this.claudeConfigs));
2327
+
2328
+ const configNames = Object.keys(this.claudeConfigs);
2329
+ if (configNames.length > 0) {
2330
+ this.currentClaudeConfig = configNames[0];
2331
+ }
2332
+ } catch (e) {
2333
+ console.error('加载 Claude 配置失败:', e);
2334
+ }
2335
+ }
2336
+ this.loadAll();
2337
+ },
2338
+ methods: {
2339
+ async loadAll() {
2340
+ this.loading = true;
2341
+ this.initError = '';
2342
+ try {
2343
+ const [statusRes, listRes, modelsRes] = await Promise.all([
2344
+ api('status'),
2345
+ api('list'),
2346
+ api('models')
2347
+ ]);
2348
+
2349
+ if (statusRes.error) {
2350
+ this.initError = statusRes.error;
2351
+ } else {
2352
+ this.currentProvider = statusRes.provider;
2353
+ this.currentModel = statusRes.model;
2354
+ this.providersList = listRes.providers;
2355
+ this.models = modelsRes.models;
2356
+ if (statusRes.configReady === false) {
2357
+ this.showMessage(statusRes.configNotice || '未检测到 config.toml,已加载默认模板;请在模板编辑器确认后创建。', 'info');
2358
+ }
2359
+ if (statusRes.initNotice) {
2360
+ this.showMessage(statusRes.initNotice, 'info');
2361
+ }
2362
+ }
2363
+ } catch (e) {
2364
+ this.initError = '连接失败: ' + e.message;
2365
+ } finally {
2366
+ this.loading = false;
2367
+ }
2368
+ },
2369
+
2370
+ switchConfigMode(mode) {
2371
+ this.configMode = mode;
2372
+ if (mode === 'sessions' && this.sessionsList.length === 0) {
2373
+ this.loadSessions();
2374
+ }
2375
+ },
2376
+
2377
+ getSessionExportKey(session) {
2378
+ return `${session.source || 'unknown'}:${session.sessionId || ''}:${session.filePath || ''}`;
2379
+ },
2380
+
2381
+ isResumeCommandAvailable(session) {
2382
+ if (!session) return false;
2383
+ const source = String(session.source || '').trim().toLowerCase();
2384
+ const sessionId = typeof session.sessionId === 'string' ? session.sessionId.trim() : '';
2385
+ return source === 'codex' && !!sessionId;
2386
+ },
2387
+
2388
+ buildResumeCommand(session) {
2389
+ const sessionId = session && session.sessionId ? String(session.sessionId).trim() : '';
2390
+ return `codex resume ${this.quoteResumeArg(sessionId)}`;
2391
+ },
2392
+
2393
+ quoteResumeArg(value) {
2394
+ const text = typeof value === 'string' ? value : String(value || '');
2395
+ if (!text) return "''";
2396
+ if (/^[a-zA-Z0-9._-]+$/.test(text)) return text;
2397
+ const escaped = text.replace(/'/g, "'\\''");
2398
+ return `'${escaped}'`;
2399
+ },
2400
+
2401
+ fallbackCopyText(text) {
2402
+ let textarea = null;
2403
+ try {
2404
+ textarea = document.createElement('textarea');
2405
+ textarea.value = text;
2406
+ textarea.setAttribute('readonly', '');
2407
+ textarea.style.position = 'fixed';
2408
+ textarea.style.top = '-9999px';
2409
+ textarea.style.left = '-9999px';
2410
+ textarea.style.opacity = '0';
2411
+ document.body.appendChild(textarea);
2412
+ textarea.select();
2413
+ textarea.setSelectionRange(0, textarea.value.length);
2414
+ return document.execCommand('copy');
2415
+ } catch (e) {
2416
+ return false;
2417
+ } finally {
2418
+ if (textarea && textarea.parentNode) {
2419
+ textarea.parentNode.removeChild(textarea);
2420
+ }
2421
+ }
2422
+ },
2423
+
2424
+ async copyResumeCommand(session) {
2425
+ if (!this.isResumeCommandAvailable(session)) {
2426
+ this.showMessage('当前会话不支持生成恢复命令', 'error');
2427
+ return;
2428
+ }
2429
+ const command = this.buildResumeCommand(session);
2430
+ const ok = this.fallbackCopyText(command);
2431
+ if (ok) {
2432
+ this.showMessage('已复制恢复命令', 'success');
2433
+ return;
2434
+ }
2435
+ try {
2436
+ if (navigator.clipboard && window.isSecureContext) {
2437
+ await navigator.clipboard.writeText(command);
2438
+ this.showMessage('已复制恢复命令', 'success');
2439
+ return;
2440
+ }
2441
+ } catch (e) {
2442
+ // keep fallback failure message
2443
+ }
2444
+ this.showMessage('复制失败,请手动复制命令', 'error');
2445
+ },
2446
+
2447
+ normalizeSessionPathValue(value) {
2448
+ if (typeof value !== 'string') return '';
2449
+ return value.trim();
2450
+ },
2451
+
2452
+ mergeSessionPathOptions(baseList = [], incomingList = []) {
2453
+ const merged = [];
2454
+ const seen = new Set();
2455
+ const append = (items) => {
2456
+ if (!Array.isArray(items)) return;
2457
+ for (const item of items) {
2458
+ const value = this.normalizeSessionPathValue(item);
2459
+ if (!value) continue;
2460
+ const key = value.toLowerCase();
2461
+ if (seen.has(key)) continue;
2462
+ seen.add(key);
2463
+ merged.push(value);
2464
+ }
2465
+ };
2466
+
2467
+ append(baseList);
2468
+ append(incomingList);
2469
+ return merged;
2470
+ },
2471
+
2472
+ extractPathOptionsFromSessions(sessions) {
2473
+ const paths = [];
2474
+ if (!Array.isArray(sessions)) {
2475
+ return paths;
2476
+ }
2477
+
2478
+ const seen = new Set();
2479
+ for (const session of sessions) {
2480
+ const value = this.normalizeSessionPathValue(session && session.cwd ? session.cwd : '');
2481
+ if (!value) continue;
2482
+ const key = value.toLowerCase();
2483
+ if (seen.has(key)) continue;
2484
+ seen.add(key);
2485
+ paths.push(value);
2486
+ }
2487
+ return paths;
2488
+ },
2489
+
2490
+ syncSessionPathOptionsForSource(source, nextOptions, mergeWithExisting = false) {
2491
+ const targetSource = source === 'codex' || source === 'claude' ? source : 'all';
2492
+ const current = Array.isArray(this.sessionPathOptionsMap[targetSource])
2493
+ ? this.sessionPathOptionsMap[targetSource]
2494
+ : [];
2495
+ const merged = mergeWithExisting
2496
+ ? this.mergeSessionPathOptions(current, nextOptions)
2497
+ : this.mergeSessionPathOptions([], nextOptions);
2498
+ this.sessionPathOptionsMap = {
2499
+ ...this.sessionPathOptionsMap,
2500
+ [targetSource]: merged
2501
+ };
2502
+ this.refreshSessionPathOptions(targetSource);
2503
+ },
2504
+
2505
+ refreshSessionPathOptions(source) {
2506
+ const targetSource = source === 'codex' || source === 'claude' ? source : 'all';
2507
+ const base = Array.isArray(this.sessionPathOptionsMap[targetSource])
2508
+ ? [...this.sessionPathOptionsMap[targetSource]]
2509
+ : [];
2510
+ const selected = this.normalizeSessionPathValue(this.sessionPathFilter);
2511
+ if (selected && !base.some(item => item.toLowerCase() === selected.toLowerCase())) {
2512
+ base.unshift(selected);
2513
+ }
2514
+ if (targetSource === this.sessionFilterSource) {
2515
+ this.sessionPathOptions = base;
2516
+ }
2517
+ },
2518
+
2519
+ async loadSessionPathOptions(options = {}) {
2520
+ const source = options.source === 'codex' || options.source === 'claude'
2521
+ ? options.source
2522
+ : 'all';
2523
+ const forceRefresh = !!options.forceRefresh;
2524
+ const loaded = !!this.sessionPathOptionsLoadedMap[source];
2525
+ if (!forceRefresh && loaded) {
2526
+ return;
2527
+ }
2528
+
2529
+ const requestSeq = ++this.sessionPathRequestSeq;
2530
+ this.sessionPathOptionsLoading = true;
2531
+ try {
2532
+ const res = await api('list-session-paths', {
2533
+ source,
2534
+ limit: 500,
2535
+ forceRefresh
2536
+ });
2537
+ if (requestSeq !== this.sessionPathRequestSeq) {
2538
+ return;
2539
+ }
2540
+ if (res && !res.error && Array.isArray(res.paths)) {
2541
+ this.syncSessionPathOptionsForSource(source, res.paths, true);
2542
+ this.sessionPathOptionsLoadedMap = {
2543
+ ...this.sessionPathOptionsLoadedMap,
2544
+ [source]: true
2545
+ };
2546
+ }
2547
+ } catch (_) {
2548
+ // 路径补全失败不影响会话主流程
2549
+ } finally {
2550
+ if (requestSeq === this.sessionPathRequestSeq) {
2551
+ this.sessionPathOptionsLoading = false;
2552
+ }
2553
+ }
2554
+ },
2555
+
2556
+ async onSessionSourceChange() {
2557
+ this.refreshSessionPathOptions(this.sessionFilterSource);
2558
+ await this.loadSessions();
2559
+ },
2560
+
2561
+ async onSessionPathFilterChange() {
2562
+ await this.loadSessions();
2563
+ },
2564
+
2565
+ async onSessionFilterChange() {
2566
+ await this.loadSessions();
2567
+ },
2568
+
2569
+ async clearSessionFilters() {
2570
+ this.sessionFilterSource = 'all';
2571
+ this.sessionPathFilter = '';
2572
+ this.sessionQuery = '';
2573
+ this.sessionRoleFilter = 'all';
2574
+ this.sessionTimePreset = 'all';
2575
+ await this.onSessionSourceChange();
2576
+ },
2577
+
2578
+ getRecordKey(message) {
2579
+ if (!message || !Number.isInteger(message.recordLineIndex) || message.recordLineIndex < 0) {
2580
+ return '';
2581
+ }
2582
+ return String(message.recordLineIndex);
2583
+ },
2584
+
2585
+ getRecordRenderKey(message, idx) {
2586
+ const recordKey = this.getRecordKey(message);
2587
+ if (recordKey) {
2588
+ return `record-${recordKey}`;
2589
+ }
2590
+ return `record-fallback-${idx}-${message && message.timestamp ? message.timestamp : ''}`;
2591
+ },
2592
+
2593
+ syncActiveSessionMessageCount(messageCount) {
2594
+ if (!Number.isFinite(messageCount) || messageCount < 0) return;
2595
+ if (this.activeSession) {
2596
+ this.activeSession.messageCount = messageCount;
2597
+ }
2598
+ const activeKey = this.activeSession ? this.getSessionExportKey(this.activeSession) : '';
2599
+ if (!activeKey) return;
2600
+ const matched = this.sessionsList.find(item => this.getSessionExportKey(item) === activeKey);
2601
+ if (matched) {
2602
+ matched.messageCount = messageCount;
2603
+ }
2604
+ },
2605
+
2606
+ async loadSessions() {
2607
+ if (this.sessionsLoading) return;
2608
+ this.sessionsLoading = true;
2609
+ this.activeSessionDetailError = '';
2610
+ const query = this.sessionQuery;
2611
+ try {
2612
+ const res = await api('list-sessions', {
2613
+ source: this.sessionFilterSource,
2614
+ pathFilter: this.sessionPathFilter,
2615
+ query,
2616
+ queryMode: 'and',
2617
+ queryScope: 'summary',
2618
+ roleFilter: this.sessionRoleFilter,
2619
+ timeRangePreset: this.sessionTimePreset,
2620
+ limit: 200,
2621
+ forceRefresh: true
2622
+ });
2623
+ if (res.error) {
2624
+ this.showMessage(res.error, 'error');
2625
+ this.sessionsList = [];
2626
+ this.activeSession = null;
2627
+ this.activeSessionMessages = [];
2628
+ this.activeSessionDetailClipped = false;
2629
+ } else {
2630
+ this.sessionsList = Array.isArray(res.sessions) ? res.sessions : [];
2631
+ this.syncSessionPathOptionsForSource(
2632
+ this.sessionFilterSource,
2633
+ this.extractPathOptionsFromSessions(this.sessionsList),
2634
+ true
2635
+ );
2636
+ if (this.sessionsList.length === 0) {
2637
+ this.activeSession = null;
2638
+ this.activeSessionMessages = [];
2639
+ this.activeSessionDetailClipped = false;
2640
+ } else {
2641
+ const oldKey = this.activeSession ? this.getSessionExportKey(this.activeSession) : '';
2642
+ const matched = this.sessionsList.find(item => this.getSessionExportKey(item) === oldKey);
2643
+ this.activeSession = matched || this.sessionsList[0];
2644
+ await this.loadActiveSessionDetail();
2645
+ }
2646
+ void this.loadSessionPathOptions({ source: this.sessionFilterSource });
2647
+ }
2648
+ } catch (e) {
2649
+ this.sessionsList = [];
2650
+ this.activeSession = null;
2651
+ this.activeSessionMessages = [];
2652
+ this.activeSessionDetailClipped = false;
2653
+ this.showMessage('加载会话失败: ' + e.message, 'error');
2654
+ } finally {
2655
+ this.sessionsLoading = false;
2656
+ }
2657
+ },
2658
+
2659
+ async selectSession(session) {
2660
+ if (!session) return;
2661
+ if (this.activeSession && this.getSessionExportKey(this.activeSession) === this.getSessionExportKey(session)) return;
2662
+ this.activeSession = session;
2663
+ this.activeSessionMessages = [];
2664
+ this.activeSessionDetailError = '';
2665
+ this.activeSessionDetailClipped = false;
2666
+ await this.loadActiveSessionDetail();
2667
+ },
2668
+
2669
+ async loadActiveSessionDetail() {
2670
+ if (!this.activeSession) {
2671
+ this.activeSessionMessages = [];
2672
+ this.activeSessionDetailError = '';
2673
+ this.activeSessionDetailClipped = false;
2674
+ return;
2675
+ }
2676
+
2677
+ const requestSeq = ++this.sessionDetailRequestSeq;
2678
+ this.sessionDetailLoading = true;
2679
+ this.activeSessionDetailError = '';
2680
+ try {
2681
+ const res = await api('session-detail', {
2682
+ source: this.activeSession.source,
2683
+ sessionId: this.activeSession.sessionId,
2684
+ filePath: this.activeSession.filePath,
2685
+ messageLimit: 300
2686
+ });
2687
+
2688
+ if (requestSeq !== this.sessionDetailRequestSeq) {
2689
+ return;
2690
+ }
2691
+
2692
+ if (res.error) {
2693
+ this.activeSessionMessages = [];
2694
+ this.activeSessionDetailClipped = false;
2695
+ this.activeSessionDetailError = res.error;
2696
+ return;
2697
+ }
2698
+
2699
+ this.activeSessionMessages = Array.isArray(res.messages) ? res.messages : [];
2700
+ this.activeSessionDetailClipped = !!res.clipped;
2701
+ if (res.updatedAt) {
2702
+ this.activeSession.updatedAt = res.updatedAt;
2703
+ }
2704
+ if (res.cwd) {
2705
+ this.activeSession.cwd = res.cwd;
2706
+ }
2707
+ if (Number.isFinite(res.totalMessages)) {
2708
+ this.syncActiveSessionMessageCount(res.totalMessages);
2709
+ }
2710
+ } catch (e) {
2711
+ if (requestSeq !== this.sessionDetailRequestSeq) {
2712
+ return;
2713
+ }
2714
+ this.activeSessionMessages = [];
2715
+ this.activeSessionDetailClipped = false;
2716
+ this.activeSessionDetailError = '加载会话内容失败: ' + e.message;
2717
+ } finally {
2718
+ if (requestSeq === this.sessionDetailRequestSeq) {
2719
+ this.sessionDetailLoading = false;
2720
+ }
2721
+ }
2722
+ },
2723
+
2724
+ downloadTextFile(fileName, content) {
2725
+ const blob = new Blob([content], { type: 'text/markdown;charset=utf-8' });
2726
+ const url = URL.createObjectURL(blob);
2727
+ const link = document.createElement('a');
2728
+ link.href = url;
2729
+ link.download = fileName;
2730
+ link.click();
2731
+ URL.revokeObjectURL(url);
2732
+ },
2733
+
2734
+ async exportSession(session) {
2735
+ const key = this.getSessionExportKey(session);
2736
+ if (this.sessionExporting[key]) return;
2737
+
2738
+ this.sessionExporting[key] = true;
2739
+ try {
2740
+ const res = await api('export-session', {
2741
+ source: session.source,
2742
+ sessionId: session.sessionId,
2743
+ filePath: session.filePath
2744
+ });
2745
+ if (res.error) {
2746
+ this.showMessage(res.error, 'error');
2747
+ return;
2748
+ }
2749
+
2750
+ const fileName = res.fileName || `${session.source || 'session'}-${session.sessionId || Date.now()}.md`;
2751
+ this.downloadTextFile(fileName, res.content || '');
2752
+ this.showMessage('会话导出完成', 'success');
2753
+ } catch (e) {
2754
+ this.showMessage('导出失败: ' + e.message, 'error');
2755
+ } finally {
2756
+ this.sessionExporting[key] = false;
2757
+ }
2758
+ },
2759
+
2760
+ async switchProvider(name) {
2761
+ this.currentProvider = name;
2762
+ await this.openConfigTemplateEditor();
2763
+ },
2764
+
2765
+ async onModelChange() {
2766
+ await this.openConfigTemplateEditor();
2767
+ },
2768
+
2769
+ escapeTomlString(value) {
2770
+ return String(value || '')
2771
+ .replace(/\\/g, '\\\\')
2772
+ .replace(/"/g, '\\"');
2773
+ },
2774
+
2775
+ async openConfigTemplateEditor(options = {}) {
2776
+ try {
2777
+ const res = await api('get-config-template', {
2778
+ provider: this.currentProvider,
2779
+ model: this.currentModel
2780
+ });
2781
+ if (res.error) {
2782
+ this.showMessage(res.error, 'error');
2783
+ return;
2784
+ }
2785
+ let template = res.template || '';
2786
+ const appendHint = typeof options.appendHint === 'string' ? options.appendHint.trim() : '';
2787
+ const appendBlock = typeof options.appendBlock === 'string' ? options.appendBlock.trim() : '';
2788
+ if (appendHint) {
2789
+ template = `${template.trimEnd()}\n\n# -------------------------------\n# ${appendHint}\n# -------------------------------\n`;
2790
+ }
2791
+ if (appendBlock) {
2792
+ template = `${template.trimEnd()}\n\n${appendBlock}\n`;
2793
+ }
2794
+ this.configTemplateContent = template;
2795
+ this.showConfigTemplateModal = true;
2796
+ } catch (e) {
2797
+ this.showMessage('加载模板失败: ' + e.message, 'error');
2798
+ }
2799
+ },
2800
+
2801
+ closeConfigTemplateModal() {
2802
+ this.showConfigTemplateModal = false;
2803
+ this.configTemplateContent = '';
2804
+ },
2805
+
2806
+ async applyConfigTemplate() {
2807
+ if (!this.configTemplateContent || !this.configTemplateContent.trim()) {
2808
+ this.showMessage('模板内容不能为空', 'error');
2809
+ return;
2810
+ }
2811
+
2812
+ this.configTemplateApplying = true;
2813
+ try {
2814
+ const res = await api('apply-config-template', {
2815
+ template: this.configTemplateContent
2816
+ });
2817
+ if (res.error) {
2818
+ this.showMessage(res.error, 'error');
2819
+ return;
2820
+ }
2821
+ this.showMessage('模板已应用到 config.toml', 'success');
2822
+ this.closeConfigTemplateModal();
2823
+ await this.loadAll();
2824
+ } catch (e) {
2825
+ this.showMessage('应用模板失败: ' + e.message, 'error');
2826
+ } finally {
2827
+ this.configTemplateApplying = false;
2828
+ }
2829
+ },
2830
+
2831
+ async openAgentsEditor() {
2832
+ this.agentsLoading = true;
2833
+ try {
2834
+ const res = await api('get-agents-file');
2835
+ if (res.error) {
2836
+ this.showMessage(res.error, 'error');
2837
+ return;
2838
+ }
2839
+ this.agentsContent = res.content || '';
2840
+ this.agentsPath = res.path || '';
2841
+ this.agentsExists = !!res.exists;
2842
+ this.agentsLineEnding = res.lineEnding === '\r\n' ? '\r\n' : '\n';
2843
+ this.showAgentsModal = true;
2844
+ } catch (e) {
2845
+ this.showMessage('加载 AGENTS.md 失败: ' + e.message, 'error');
2846
+ } finally {
2847
+ this.agentsLoading = false;
2848
+ }
2849
+ },
2850
+
2851
+ closeAgentsModal() {
2852
+ this.showAgentsModal = false;
2853
+ this.agentsContent = '';
2854
+ this.agentsPath = '';
2855
+ this.agentsExists = false;
2856
+ this.agentsLineEnding = '\n';
2857
+ this.agentsSaving = false;
2858
+ },
2859
+
2860
+ async applyAgentsContent() {
2861
+ this.agentsSaving = true;
2862
+ try {
2863
+ const res = await api('apply-agents-file', {
2864
+ content: this.agentsContent,
2865
+ lineEnding: this.agentsLineEnding
2866
+ });
2867
+ if (res.error) {
2868
+ this.showMessage(res.error, 'error');
2869
+ return;
2870
+ }
2871
+ this.showMessage('AGENTS.md 已保存', 'success');
2872
+ this.closeAgentsModal();
2873
+ } catch (e) {
2874
+ this.showMessage('保存 AGENTS.md 失败: ' + e.message, 'error');
2875
+ } finally {
2876
+ this.agentsSaving = false;
2877
+ }
2878
+ },
2879
+
2880
+ async addProvider() {
2881
+ if (!this.newProvider.name || !this.newProvider.url) {
2882
+ return this.showMessage('名称和URL必填', 'error');
2883
+ }
2884
+ const name = this.newProvider.name.trim();
2885
+ if (!name) {
2886
+ return this.showMessage('名称不能为空', 'error');
2887
+ }
2888
+ if (this.providersList.some(item => item.name === name)) {
2889
+ return this.showMessage('提供商已存在', 'error');
2890
+ }
2891
+
2892
+ const safeName = this.escapeTomlString(name);
2893
+ const safeUrl = this.escapeTomlString(this.newProvider.url.trim());
2894
+ const safeKey = this.escapeTomlString(this.newProvider.key || '');
2895
+ 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`;
2896
+
2897
+ this.currentProvider = name;
2898
+ this.showMessage('已生成新增模板,请确认后应用', 'info');
2899
+ this.closeAddModal();
2900
+ await this.openConfigTemplateEditor({
2901
+ appendHint: `新增 provider: ${name}(请检查字段后应用)`,
2902
+ appendBlock: newProviderBlock
2903
+ });
2904
+ },
2905
+
2906
+ async deleteProvider(name) {
2907
+ if (!confirm(`确定删除提供商 "${name}"?`)) return;
2908
+ this.showMessage('请在模板中手动删除该 provider 配置块后应用', 'info');
2909
+ await this.openConfigTemplateEditor({
2910
+ appendHint: `请手动删除 [model_providers.${name}] 配置块,并确认 model_provider 指向有效 provider`
2911
+ });
2912
+ },
2913
+
2914
+ openEditModal(provider) {
2915
+ this.editingProvider = {
2916
+ name: provider.name,
2917
+ url: provider.url || '',
2918
+ key: ''
2919
+ };
2920
+ this.showEditModal = true;
2921
+ },
2922
+
2923
+ async updateProvider() {
2924
+ if (!this.editingProvider.url) {
2925
+ return this.showMessage('URL 必填', 'error');
2926
+ }
2927
+
2928
+ const name = this.editingProvider.name;
2929
+ const safeUrl = this.escapeTomlString(this.editingProvider.url.trim());
2930
+ const safeKey = this.escapeTomlString(this.editingProvider.key || '');
2931
+ this.closeEditModal();
2932
+ this.showMessage('已生成更新模板,请确认后应用', 'info');
2933
+ await this.openConfigTemplateEditor({
2934
+ appendHint: `请将 [model_providers.${name}] 中 base_url 更新为 ${safeUrl}${safeKey ? ',并更新 preferred_auth_method' : ''}`
2935
+ });
2936
+ },
2937
+
2938
+ closeEditModal() {
2939
+ this.showEditModal = false;
2940
+ this.editingProvider = { name: '', url: '', key: '' };
2941
+ },
2942
+
2943
+ async addModel() {
2944
+ if (!this.newModelName || !this.newModelName.trim()) {
2945
+ return this.showMessage('请输入模型名称', 'error');
2946
+ }
2947
+ const res = await api('add-model', { model: this.newModelName.trim() });
2948
+ if (res.error) {
2949
+ this.showMessage(res.error, 'error');
2950
+ } else {
2951
+ this.showMessage('已添加', 'success');
2952
+ this.closeModelModal();
2953
+ await this.loadAll();
2954
+ }
2955
+ },
2956
+
2957
+ async removeModel(model) {
2958
+ if (!confirm(`确定删除模型 "${model}"?`)) return;
2959
+ const res = await api('delete-model', { model });
2960
+ if (res.error) {
2961
+ this.showMessage(res.error, 'error');
2962
+ } else {
2963
+ this.showMessage('已删除', 'success');
2964
+ await this.loadAll();
2965
+ }
2966
+ },
2967
+
2968
+ closeAddModal() {
2969
+ this.showAddModal = false;
2970
+ this.newProvider = { name: '', url: '', key: '' };
2971
+ },
2972
+
2973
+ closeModelModal() {
2974
+ this.showModelModal = false;
2975
+ this.newModelName = '';
2976
+ },
2977
+
2978
+ formatKey(key) {
2979
+ if (!key) return '(未设置)';
2980
+ if (key.length > 10) {
2981
+ return key.substring(0, 3) + '****' + key.substring(key.length - 3);
2982
+ }
2983
+ return '****';
2984
+ },
2985
+
2986
+ displayApiKey(configName) {
2987
+ const key = this.claudeConfigs[configName]?.apiKey;
2988
+ return this.formatKey(key);
2989
+ },
2990
+
2991
+ switchClaudeConfig(name) {
2992
+ this.currentClaudeConfig = name;
2993
+ },
2994
+
2995
+ saveClaudeConfigs() {
2996
+ localStorage.setItem('claudeConfigs', JSON.stringify(this.claudeConfigs));
2997
+ },
2998
+
2999
+ openEditConfigModal(name) {
3000
+ const config = this.claudeConfigs[name];
3001
+ this.editingConfig = {
3002
+ name: name,
3003
+ apiKey: config.apiKey || '',
3004
+ baseUrl: config.baseUrl || '',
3005
+ model: config.model || ''
3006
+ };
3007
+ this.showEditConfigModal = true;
3008
+ },
3009
+
3010
+ updateConfig() {
3011
+ const name = this.editingConfig.name;
3012
+ this.claudeConfigs[name] = {
3013
+ apiKey: this.editingConfig.apiKey,
3014
+ baseUrl: this.editingConfig.baseUrl,
3015
+ model: this.editingConfig.model,
3016
+ hasKey: !!this.editingConfig.apiKey
3017
+ };
3018
+ this.saveClaudeConfigs();
3019
+ this.showMessage('配置已更新', 'success');
3020
+ this.closeEditConfigModal();
3021
+ },
3022
+
3023
+ closeEditConfigModal() {
3024
+ this.showEditConfigModal = false;
3025
+ this.editingConfig = { name: '', apiKey: '', baseUrl: '', model: '' };
3026
+ },
3027
+
3028
+ async saveAndApplyConfig() {
3029
+ const name = this.editingConfig.name;
3030
+ this.claudeConfigs[name] = {
3031
+ apiKey: this.editingConfig.apiKey,
3032
+ baseUrl: this.editingConfig.baseUrl,
3033
+ model: this.editingConfig.model,
3034
+ hasKey: !!this.editingConfig.apiKey
3035
+ };
3036
+ this.saveClaudeConfigs();
3037
+
3038
+ const config = this.claudeConfigs[name];
3039
+ if (!config.apiKey) {
3040
+ return this.showMessage('请先输入 API Key', 'error');
3041
+ }
3042
+
3043
+ const res = await api('apply-claude-config', { config });
3044
+ if (res.error || res.success === false) {
3045
+ this.showMessage(res.error || '应用 Claude 配置失败', 'error');
3046
+ } else {
3047
+ const targetTip = res.targetPath ? `(${res.targetPath})` : '';
3048
+ this.showMessage(`已保存并应用到 Claude 配置${targetTip}`, 'success');
3049
+ this.closeEditConfigModal();
3050
+ }
3051
+ },
3052
+
3053
+ async saveAndApplyEnvCompat() {
3054
+ const name = this.editingConfig.name;
3055
+ this.claudeConfigs[name] = {
3056
+ apiKey: this.editingConfig.apiKey,
3057
+ baseUrl: this.editingConfig.baseUrl,
3058
+ model: this.editingConfig.model,
3059
+ hasKey: !!this.editingConfig.apiKey
3060
+ };
3061
+ this.saveClaudeConfigs();
3062
+
3063
+ const config = this.claudeConfigs[name];
3064
+ if (!config.apiKey) {
3065
+ return this.showMessage('请先输入 API Key', 'error');
3066
+ }
3067
+
3068
+ const res = await api('apply-env', { config });
3069
+ if (res.error || res.success === false) {
3070
+ this.showMessage(res.error || '应用环境变量失败', 'error');
3071
+ } else {
3072
+ this.showMessage('已保存并应用到系统环境变量(兼容模式)', 'success');
3073
+ this.closeEditConfigModal();
3074
+ }
3075
+ },
3076
+
3077
+ addClaudeConfig() {
3078
+ if (!this.newClaudeConfig.name || !this.newClaudeConfig.name.trim()) {
3079
+ return this.showMessage('请输入配置名称', 'error');
3080
+ }
3081
+ const name = this.newClaudeConfig.name.trim();
3082
+ if (this.claudeConfigs[name]) {
3083
+ return this.showMessage('配置名称已存在', 'error');
3084
+ }
3085
+
3086
+ this.claudeConfigs[name] = {
3087
+ apiKey: this.newClaudeConfig.apiKey,
3088
+ baseUrl: this.newClaudeConfig.baseUrl,
3089
+ model: this.newClaudeConfig.model,
3090
+ hasKey: !!this.newClaudeConfig.apiKey
3091
+ };
3092
+
3093
+ this.currentClaudeConfig = name;
3094
+ this.saveClaudeConfigs();
3095
+ this.showMessage('配置已添加', 'success');
3096
+ this.closeClaudeConfigModal();
3097
+ },
3098
+
3099
+ deleteClaudeConfig(name) {
3100
+ if (Object.keys(this.claudeConfigs).length <= 1) {
3101
+ return this.showMessage('至少保留一个配置', 'error');
3102
+ }
3103
+
3104
+ if (!confirm(`确定删除配置 "${name}"?`)) return;
3105
+
3106
+ delete this.claudeConfigs[name];
3107
+ if (this.currentClaudeConfig === name) {
3108
+ this.currentClaudeConfig = Object.keys(this.claudeConfigs)[0];
3109
+ }
3110
+ this.saveClaudeConfigs();
3111
+ this.showMessage('配置已删除', 'success');
3112
+ },
3113
+
3114
+ async applyClaudeConfig(name) {
3115
+ this.currentClaudeConfig = name;
3116
+ const config = this.claudeConfigs[name];
3117
+
3118
+ if (!config.apiKey) {
3119
+ return this.showMessage('该配置未设置 API Key,请先编辑', 'error');
3120
+ }
3121
+
3122
+ const res = await api('apply-claude-config', { config });
3123
+ if (res.error || res.success === false) {
3124
+ this.showMessage(res.error || '应用 Claude 配置失败', 'error');
3125
+ } else {
3126
+ const targetTip = res.targetPath ? `(${res.targetPath})` : '';
3127
+ this.showMessage(`已应用配置到 Claude 设置: ${name}${targetTip}`, 'success');
3128
+ }
3129
+ },
3130
+
3131
+ closeClaudeConfigModal() {
3132
+ this.showClaudeConfigModal = false;
3133
+ this.newClaudeConfig = {
3134
+ name: '',
3135
+ apiKey: '',
3136
+ baseUrl: 'https://open.bigmodel.cn/api/anthropic',
3137
+ model: 'glm-4.7'
3138
+ };
3139
+ },
3140
+
3141
+ formatLatency(result) {
3142
+ if (!result) return '';
3143
+ if (!result.ok) return result.status ? `ERR ${result.status}` : 'ERR';
3144
+ const ms = typeof result.durationMs === 'number' ? result.durationMs : 0;
3145
+ return `${ms}ms`;
3146
+ },
3147
+
3148
+ async runSpeedTest(name) {
3149
+ if (!name || this.speedLoading[name]) return;
3150
+ this.speedLoading[name] = true;
3151
+ const res = await api('speed-test', { name });
3152
+ if (res.error) {
3153
+ this.speedResults[name] = { ok: false, error: res.error };
3154
+ this.showMessage(res.error, 'error');
3155
+ } else {
3156
+ this.speedResults[name] = res;
3157
+ const status = res.status ? ` (${res.status})` : '';
3158
+ this.showMessage(`Speed ${name}: ${this.formatLatency(res)}${status}`, 'success');
3159
+ }
3160
+ this.speedLoading[name] = false;
3161
+ },
3162
+
3163
+ showMessage(text, type) {
3164
+ this.message = text;
3165
+ this.messageType = type || 'info';
3166
+ setTimeout(() => {
3167
+ this.message = '';
3168
+ }, 3000);
3169
+ }
3170
+ }
3171
+ });
3172
+
3173
+ app.mount('#app');
3174
+ </script>
3175
+ </body>
3176
+ </html>
3177
+
3178
+
3179
+
3180
+
3181
+
3182
+
3183
+