@gird/ccli 2.17.0 → 2.19.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/README.md +53 -2
  2. package/dist/agent/ai.js +3 -50
  3. package/dist/agent/ai.js.map +1 -1
  4. package/dist/agent/os.js +69 -0
  5. package/dist/agent/os.js.map +1 -0
  6. package/dist/agent/providers/anthropic.js +56 -5
  7. package/dist/agent/providers/anthropic.js.map +1 -1
  8. package/dist/agent/providers/gemini.js +10 -93
  9. package/dist/agent/providers/gemini.js.map +1 -1
  10. package/dist/agent/providers/openai.js +9 -1
  11. package/dist/agent/providers/openai.js.map +1 -1
  12. package/dist/agent/tool-projection.js +57 -0
  13. package/dist/agent/tool-projection.js.map +1 -0
  14. package/dist/cli/args/index.js +40 -0
  15. package/dist/cli/args/index.js.map +1 -1
  16. package/dist/cli/clipboard-image.js +51 -0
  17. package/dist/cli/clipboard-image.js.map +1 -0
  18. package/dist/cli/image-compress.js +62 -0
  19. package/dist/cli/image-compress.js.map +1 -0
  20. package/dist/cli/image-reference.js +190 -0
  21. package/dist/cli/image-reference.js.map +1 -0
  22. package/dist/cli/repl/index.js +52 -3
  23. package/dist/cli/repl/index.js.map +1 -1
  24. package/dist/cli/run/index.js +75 -12
  25. package/dist/cli/run/index.js.map +1 -1
  26. package/dist/cli/run/task.js +37 -0
  27. package/dist/cli/run/task.js.map +1 -1
  28. package/dist/cli/session/checkpoint.js +5 -4
  29. package/dist/cli/session/checkpoint.js.map +1 -1
  30. package/dist/cli/slash/config/model.js +30 -9
  31. package/dist/cli/slash/config/model.js.map +1 -1
  32. package/dist/cli/slash/index.js +14 -0
  33. package/dist/cli/slash/index.js.map +1 -1
  34. package/dist/cli/slash/session/replay.js +118 -0
  35. package/dist/cli/slash/session/replay.js.map +1 -0
  36. package/dist/cli/slash/system/agent.js +1 -0
  37. package/dist/cli/slash/system/agent.js.map +1 -1
  38. package/dist/cli/slash/system/goal.js +1 -0
  39. package/dist/cli/slash/system/goal.js.map +1 -1
  40. package/dist/cli/slash/system/help.js +1 -0
  41. package/dist/cli/slash/system/help.js.map +1 -1
  42. package/dist/cli/slash/system/plan.js +3 -0
  43. package/dist/cli/slash/system/plan.js.map +1 -1
  44. package/dist/config/config.js +32 -1
  45. package/dist/config/config.js.map +1 -1
  46. package/dist/config/plugins.js +378 -0
  47. package/dist/config/plugins.js.map +1 -0
  48. package/dist/config/skills.js +39 -2
  49. package/dist/config/skills.js.map +1 -1
  50. package/dist/harness/index.js +29 -1
  51. package/dist/harness/index.js.map +1 -1
  52. package/dist/harness/records.js +98 -0
  53. package/dist/harness/records.js.map +1 -0
  54. package/dist/index.js +77 -2
  55. package/dist/index.js.map +1 -1
  56. package/dist/server/index.js +338 -0
  57. package/dist/server/index.js.map +1 -0
  58. package/dist/server/sessions.js +181 -0
  59. package/dist/server/sessions.js.map +1 -0
  60. package/dist/server/webui.html +1341 -0
  61. package/dist/server/webui.js +12 -0
  62. package/dist/server/webui.js.map +1 -0
  63. package/dist/tools/bash-background.js +5 -6
  64. package/dist/tools/bash-background.js.map +1 -1
  65. package/dist/tools/bash.js +6 -7
  66. package/dist/tools/bash.js.map +1 -1
  67. package/dist/tools/edit-file.js +10 -4
  68. package/dist/tools/edit-file.js.map +1 -1
  69. package/dist/tools/glob.js +40 -6
  70. package/dist/tools/glob.js.map +1 -1
  71. package/dist/tools/grep.js +48 -8
  72. package/dist/tools/grep.js.map +1 -1
  73. package/dist/tools/list-dir.js +13 -2
  74. package/dist/tools/list-dir.js.map +1 -1
  75. package/dist/tools/read-file.js +4 -2
  76. package/dist/tools/read-file.js.map +1 -1
  77. package/dist/tools/shared.js.map +1 -1
  78. package/dist/tools/write-file.js +11 -4
  79. package/dist/tools/write-file.js.map +1 -1
  80. package/package.json +2 -2
@@ -0,0 +1,1341 @@
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>CCLI Web Console</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700&family=Fira+Code:wght@400;500&display=swap" rel="stylesheet">
10
+ <!-- Markdown 解析与代码高亮 -->
11
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
12
+ <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/highlight.js@11.9.0/styles/github-dark.min.css" id="hl-theme">
13
+ <script src="https://cdn.jsdelivr.net/npm/highlight.js@11.9.0/highlight.min.js"></script>
14
+ <style>
15
+ :root {
16
+ --bg-gradient: linear-gradient(135deg, #0f172a 0%, #1e1b4b 100%);
17
+ --panel-bg: rgba(30, 41, 59, 0.7);
18
+ --panel-border: rgba(255, 255, 255, 0.08);
19
+ --text-main: #f8fafc;
20
+ --text-muted: #94a3b8;
21
+ --primary: #6366f1;
22
+ --primary-hover: #4f46e5;
23
+ --primary-glow: rgba(99, 102, 241, 0.3);
24
+ --success: #10b981;
25
+ --danger: #ef4444;
26
+ --warning: #f59e0b;
27
+ --sidebar-width: 280px;
28
+
29
+ /* 暗色模式的默认背景与布局颜色属性 */
30
+ --auth-overlay-bg: rgba(15, 23, 42, 0.95);
31
+ --auth-input-bg: rgba(15, 23, 42, 0.6);
32
+ --sidebar-bg: rgba(15, 23, 42, 0.4);
33
+ --chat-area-bg: rgba(15, 23, 42, 0.2);
34
+ --tool-card-bg: rgba(30, 41, 59, 0.4);
35
+ --tool-body-bg: rgba(15, 23, 42, 0.3);
36
+ --tool-result-body-bg: rgba(15, 23, 42, 0.5);
37
+ --input-container-bg: rgba(15, 23, 42, 0.3);
38
+ --input-color: #fff;
39
+ --active-session-bg: rgba(99, 102, 241, 0.1);
40
+ --active-session-border: rgba(99, 102, 241, 0.2);
41
+ --code-text-color: #fff;
42
+ --btn-text-color: #fff;
43
+ }
44
+
45
+ body.light-mode {
46
+ --bg-gradient: linear-gradient(135deg, #f1f5f9 0%, #cbd5e1 100%);
47
+ --panel-bg: rgba(255, 255, 255, 0.75);
48
+ --panel-border: rgba(15, 23, 42, 0.08);
49
+ --text-main: #1e293b;
50
+ --text-muted: #64748b;
51
+ --primary: #4f46e5;
52
+ --primary-hover: #4338ca;
53
+ --primary-glow: rgba(79, 70, 229, 0.15);
54
+ --success: #10b981;
55
+ --danger: #ef4444;
56
+ --warning: #d97706;
57
+
58
+ --auth-overlay-bg: rgba(241, 245, 249, 0.95);
59
+ --auth-input-bg: rgba(0, 0, 0, 0.05);
60
+ --sidebar-bg: rgba(241, 245, 249, 0.6);
61
+ --chat-area-bg: rgba(255, 255, 255, 0.35);
62
+ --tool-card-bg: rgba(255, 255, 255, 0.8);
63
+ --tool-body-bg: rgba(248, 250, 252, 0.9);
64
+ --tool-result-body-bg: rgba(241, 245, 249, 0.95);
65
+ --input-container-bg: rgba(241, 245, 249, 0.7);
66
+ --input-color: #1e293b;
67
+ --active-session-bg: rgba(79, 70, 229, 0.08);
68
+ --active-session-border: rgba(79, 70, 229, 0.15);
69
+ --code-text-color: #334155;
70
+ --btn-text-color: #fff;
71
+ }
72
+
73
+ * {
74
+ box-sizing: border-box;
75
+ margin: 0;
76
+ padding: 0;
77
+ }
78
+
79
+ body {
80
+ font-family: 'Outfit', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
81
+ background: var(--bg-gradient);
82
+ color: var(--text-main);
83
+ height: 100vh;
84
+ display: flex;
85
+ overflow: hidden;
86
+ }
87
+
88
+ /* 授权弹窗 */
89
+ #auth-overlay {
90
+ position: fixed;
91
+ top: 0;
92
+ left: 0;
93
+ width: 100%;
94
+ height: 100%;
95
+ background: var(--auth-overlay-bg);
96
+ backdrop-filter: blur(8px);
97
+ z-index: 9999;
98
+ display: flex;
99
+ align-items: center;
100
+ justify-content: center;
101
+ transition: opacity 0.3s ease;
102
+ }
103
+
104
+ .auth-card {
105
+ background: var(--panel-bg);
106
+ border: 1px solid var(--panel-border);
107
+ border-radius: 16px;
108
+ padding: 32px;
109
+ width: 400px;
110
+ box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
111
+ text-align: center;
112
+ }
113
+
114
+ .auth-card h2 {
115
+ font-weight: 600;
116
+ margin-bottom: 12px;
117
+ font-size: 24px;
118
+ }
119
+
120
+ .auth-card p {
121
+ color: var(--text-muted);
122
+ font-size: 14px;
123
+ margin-bottom: 24px;
124
+ }
125
+
126
+ .auth-input-group {
127
+ display: flex;
128
+ flex-direction: column;
129
+ gap: 8px;
130
+ text-align: left;
131
+ margin-bottom: 24px;
132
+ }
133
+
134
+ .auth-input-group label {
135
+ font-size: 12px;
136
+ text-transform: uppercase;
137
+ letter-spacing: 0.05em;
138
+ color: var(--text-muted);
139
+ }
140
+
141
+ .auth-input-group input {
142
+ background: var(--auth-input-bg);
143
+ border: 1px solid var(--panel-border);
144
+ border-radius: 8px;
145
+ padding: 12px;
146
+ color: var(--input-color);
147
+ font-size: 16px;
148
+ outline: none;
149
+ transition: border-color 0.2s;
150
+ }
151
+
152
+ .auth-input-group input:focus {
153
+ border-color: var(--primary);
154
+ box-shadow: 0 0 0 2px var(--primary-glow);
155
+ }
156
+
157
+ .btn {
158
+ background: var(--primary);
159
+ color: #fff;
160
+ border: none;
161
+ border-radius: 8px;
162
+ padding: 12px 20px;
163
+ font-size: 15px;
164
+ font-weight: 500;
165
+ cursor: pointer;
166
+ transition: all 0.2s;
167
+ width: 100%;
168
+ }
169
+
170
+ .btn:hover {
171
+ background: var(--primary-hover);
172
+ transform: translateY(-1px);
173
+ }
174
+
175
+ /* 侧边栏 */
176
+ aside {
177
+ width: var(--sidebar-width);
178
+ background: var(--sidebar-bg);
179
+ border-right: 1px solid var(--panel-border);
180
+ display: flex;
181
+ flex-direction: column;
182
+ flex-shrink: 0;
183
+ }
184
+
185
+ .sidebar-header {
186
+ padding: 24px;
187
+ border-bottom: 1px solid var(--panel-border);
188
+ }
189
+
190
+ .logo {
191
+ font-size: 20px;
192
+ font-weight: 700;
193
+ letter-spacing: -0.02em;
194
+ background: linear-gradient(to right, #818cf8, #c084fc);
195
+ -webkit-background-clip: text;
196
+ -webkit-text-fill-color: transparent;
197
+ display: flex;
198
+ align-items: center;
199
+ gap: 8px;
200
+ }
201
+
202
+ .logo span {
203
+ font-size: 12px;
204
+ font-weight: 400;
205
+ color: var(--text-muted);
206
+ -webkit-text-fill-color: var(--text-muted);
207
+ border: 1px solid var(--panel-border);
208
+ padding: 2px 6px;
209
+ border-radius: 4px;
210
+ }
211
+
212
+ .sidebar-actions {
213
+ padding: 16px 24px;
214
+ }
215
+
216
+ .btn-new-session {
217
+ display: flex;
218
+ align-items: center;
219
+ justify-content: center;
220
+ gap: 8px;
221
+ background: rgba(99, 102, 241, 0.15);
222
+ color: #a5b4fc;
223
+ border: 1px solid rgba(99, 102, 241, 0.3);
224
+ }
225
+
226
+ .btn-new-session:hover {
227
+ background: rgba(99, 102, 241, 0.25);
228
+ color: #fff;
229
+ }
230
+
231
+ .session-list-container {
232
+ flex: 1;
233
+ overflow-y: auto;
234
+ padding: 8px 16px;
235
+ }
236
+
237
+ .session-item {
238
+ display: flex;
239
+ align-items: center;
240
+ justify-content: space-between;
241
+ padding: 12px;
242
+ border-radius: 8px;
243
+ margin-bottom: 4px;
244
+ cursor: pointer;
245
+ transition: all 0.2s;
246
+ border: 1px solid transparent;
247
+ }
248
+
249
+ .session-item:hover {
250
+ background: rgba(255, 255, 255, 0.03);
251
+ }
252
+
253
+ .session-item.active {
254
+ background: var(--active-session-bg);
255
+ border-color: var(--active-session-border);
256
+ color: var(--text-main);
257
+ }
258
+
259
+ .session-item-name {
260
+ font-size: 14px;
261
+ white-space: nowrap;
262
+ overflow: hidden;
263
+ text-overflow: ellipsis;
264
+ max-width: 180px;
265
+ }
266
+
267
+ .session-item-delete {
268
+ opacity: 0;
269
+ background: transparent;
270
+ border: none;
271
+ color: var(--text-muted);
272
+ cursor: pointer;
273
+ transition: opacity 0.2s, color 0.2s;
274
+ padding: 2px;
275
+ }
276
+
277
+ .session-item:hover .session-item-delete {
278
+ opacity: 1;
279
+ }
280
+
281
+ .session-item-delete:hover {
282
+ color: var(--danger);
283
+ }
284
+
285
+ .sidebar-footer {
286
+ padding: 16px 24px;
287
+ border-top: 1px solid var(--panel-border);
288
+ display: flex;
289
+ justify-content: space-between;
290
+ align-items: center;
291
+ }
292
+
293
+ .sidebar-footer button {
294
+ background: transparent;
295
+ border: none;
296
+ color: var(--text-muted);
297
+ cursor: pointer;
298
+ font-size: 13px;
299
+ }
300
+
301
+ .sidebar-footer button:hover {
302
+ color: var(--text-main);
303
+ }
304
+
305
+ /* 主体对话区 */
306
+ main {
307
+ flex: 1;
308
+ display: flex;
309
+ flex-direction: column;
310
+ background: var(--chat-area-bg);
311
+ position: relative;
312
+ }
313
+
314
+ header {
315
+ padding: 20px 32px;
316
+ border-bottom: 1px solid var(--panel-border);
317
+ display: flex;
318
+ align-items: center;
319
+ justify-content: space-between;
320
+ background: var(--chat-area-bg);
321
+ backdrop-filter: blur(10px);
322
+ }
323
+
324
+ .header-info h1 {
325
+ font-size: 16px;
326
+ font-weight: 600;
327
+ }
328
+
329
+ .header-info p {
330
+ font-size: 12px;
331
+ color: var(--text-muted);
332
+ margin-top: 2px;
333
+ }
334
+
335
+ .chat-area {
336
+ flex: 1;
337
+ overflow-y: auto;
338
+ padding: 32px;
339
+ display: flex;
340
+ flex-direction: column;
341
+ gap: 24px;
342
+ }
343
+
344
+ /* 消息气泡 */
345
+ .message-row {
346
+ display: flex;
347
+ width: 100%;
348
+ }
349
+
350
+ .message-row.user {
351
+ justify-content: flex-end;
352
+ }
353
+
354
+ .message {
355
+ max-width: 80%;
356
+ border-radius: 12px;
357
+ padding: 16px 20px;
358
+ font-size: 15px;
359
+ line-height: 1.6;
360
+ }
361
+
362
+ .message-row.user .message {
363
+ background: var(--primary);
364
+ color: #fff;
365
+ border-top-right-radius: 2px;
366
+ box-shadow: 0 4px 12px rgba(99, 102, 241, 0.2);
367
+ }
368
+
369
+ .message-row.assistant .message {
370
+ background: var(--panel-bg);
371
+ border: 1px solid var(--panel-border);
372
+ color: var(--text-main);
373
+ border-top-left-radius: 2px;
374
+ }
375
+
376
+ /* 工具调用和结果 */
377
+ .tool-card {
378
+ border: 1px solid var(--panel-border);
379
+ border-radius: 8px;
380
+ background: var(--tool-card-bg);
381
+ margin: 8px 0;
382
+ overflow: hidden;
383
+ width: 100%;
384
+ }
385
+
386
+ .tool-header {
387
+ padding: 10px 16px;
388
+ background: rgba(255, 255, 255, 0.02);
389
+ display: flex;
390
+ align-items: center;
391
+ justify-content: space-between;
392
+ font-size: 13px;
393
+ color: var(--text-muted);
394
+ border-bottom: 1px solid var(--panel-border);
395
+ cursor: pointer;
396
+ }
397
+
398
+ .tool-header-info {
399
+ display: flex;
400
+ align-items: center;
401
+ gap: 8px;
402
+ }
403
+
404
+ .tool-icon {
405
+ color: var(--warning);
406
+ }
407
+
408
+ .tool-name {
409
+ font-family: 'Fira Code', monospace;
410
+ color: var(--primary);
411
+ font-weight: 500;
412
+ }
413
+
414
+ .tool-body {
415
+ padding: 12px 16px;
416
+ font-family: 'Fira Code', monospace;
417
+ font-size: 12px;
418
+ background: var(--tool-body-bg);
419
+ color: var(--code-text-color);
420
+ white-space: pre-wrap;
421
+ overflow-x: auto;
422
+ }
423
+
424
+ .tool-result-header {
425
+ padding: 6px 16px;
426
+ font-size: 11px;
427
+ color: var(--text-muted);
428
+ border-top: 1px solid var(--panel-border);
429
+ background: rgba(255, 255, 255, 0.01);
430
+ }
431
+
432
+ .tool-result-body {
433
+ padding: 12px 16px;
434
+ font-family: 'Fira Code', monospace;
435
+ font-size: 12px;
436
+ background: var(--tool-result-body-bg);
437
+ color: #34d399;
438
+ white-space: pre-wrap;
439
+ overflow-x: auto;
440
+ max-height: 250px;
441
+ overflow-y: auto;
442
+ }
443
+
444
+ .tool-result-body.error {
445
+ color: var(--danger);
446
+ }
447
+
448
+ /* 审批弹窗 (Inline Card) */
449
+ .approval-card {
450
+ border: 1px solid rgba(245, 158, 11, 0.4);
451
+ background: rgba(245, 158, 11, 0.08);
452
+ border-radius: 12px;
453
+ padding: 20px;
454
+ margin: 16px 0;
455
+ display: flex;
456
+ flex-direction: column;
457
+ gap: 12px;
458
+ animation: pulse-border 2s infinite;
459
+ }
460
+
461
+ @keyframes pulse-border {
462
+ 0% { border-color: rgba(245, 158, 11, 0.4); }
463
+ 50% { border-color: rgba(245, 158, 11, 0.8); }
464
+ 100% { border-color: rgba(245, 158, 11, 0.4); }
465
+ }
466
+
467
+ .approval-header {
468
+ font-weight: 600;
469
+ color: #fbbf24;
470
+ display: flex;
471
+ align-items: center;
472
+ gap: 8px;
473
+ }
474
+
475
+ .approval-preview {
476
+ background: var(--auth-input-bg);
477
+ color: var(--code-text-color);
478
+ border: 1px solid var(--panel-border);
479
+ padding: 12px;
480
+ border-radius: 6px;
481
+ font-family: 'Fira Code', monospace;
482
+ font-size: 12px;
483
+ white-space: pre-wrap;
484
+ }
485
+
486
+ .approval-actions {
487
+ display: flex;
488
+ gap: 12px;
489
+ }
490
+
491
+ .btn-approve {
492
+ background: var(--success);
493
+ }
494
+ .btn-approve:hover {
495
+ background: #059669;
496
+ }
497
+
498
+ .btn-always {
499
+ background: var(--primary);
500
+ }
501
+ .btn-always:hover {
502
+ background: var(--primary-hover);
503
+ }
504
+
505
+ .btn-deny {
506
+ background: var(--danger);
507
+ }
508
+ .btn-deny:hover {
509
+ background: #dc2626;
510
+ }
511
+
512
+ /* 输入区 */
513
+ .input-area-container {
514
+ padding: 24px 32px 32px;
515
+ background: var(--input-container-bg);
516
+ border-top: 1px solid var(--panel-border);
517
+ }
518
+
519
+ .input-box {
520
+ position: relative;
521
+ background: var(--panel-bg);
522
+ border: 1px solid var(--panel-border);
523
+ border-radius: 12px;
524
+ padding: 12px 16px;
525
+ display: flex;
526
+ align-items: flex-end;
527
+ gap: 12px;
528
+ box-shadow: 0 4px 20px rgba(0, 0, 0, 0.2);
529
+ transition: border-color 0.2s;
530
+ }
531
+
532
+ .input-box:focus-within {
533
+ border-color: var(--primary);
534
+ box-shadow: 0 0 0 1px var(--primary), 0 4px 20px rgba(0, 0, 0, 0.2);
535
+ }
536
+
537
+ .input-box textarea {
538
+ flex: 1;
539
+ background: transparent;
540
+ border: none;
541
+ color: var(--input-color);
542
+ font-size: 15px;
543
+ font-family: inherit;
544
+ outline: none;
545
+ resize: none;
546
+ max-height: 150px;
547
+ height: 24px;
548
+ line-height: 24px;
549
+ padding: 0;
550
+ margin: 0;
551
+ overflow-y: hidden;
552
+ box-sizing: border-box;
553
+ }
554
+
555
+ .btn-send {
556
+ background: var(--primary);
557
+ color: #fff;
558
+ border: none;
559
+ border-radius: 8px;
560
+ width: 32px;
561
+ height: 32px;
562
+ display: flex;
563
+ align-items: center;
564
+ justify-content: center;
565
+ cursor: pointer;
566
+ transition: all 0.2s;
567
+ flex-shrink: 0;
568
+ }
569
+
570
+ .btn-send:hover {
571
+ background: var(--primary-hover);
572
+ transform: scale(1.05);
573
+ }
574
+
575
+ .btn-send svg {
576
+ width: 16px;
577
+ height: 16px;
578
+ fill: currentColor;
579
+ }
580
+
581
+ /* 空状态 */
582
+ .empty-state {
583
+ margin: auto;
584
+ text-align: center;
585
+ max-width: 480px;
586
+ display: flex;
587
+ flex-direction: column;
588
+ align-items: center;
589
+ gap: 16px;
590
+ }
591
+
592
+ .empty-state svg {
593
+ width: 64px;
594
+ height: 64px;
595
+ color: var(--text-muted);
596
+ opacity: 0.5;
597
+ }
598
+
599
+ .empty-state h2 {
600
+ font-weight: 600;
601
+ }
602
+
603
+ .empty-state p {
604
+ color: var(--text-muted);
605
+ font-size: 14px;
606
+ line-height: 1.6;
607
+ }
608
+
609
+ /* 自定义 Toast 提示 */
610
+ .toast-container {
611
+ position: fixed;
612
+ top: 24px;
613
+ right: 24px;
614
+ z-index: 10000;
615
+ display: flex;
616
+ flex-direction: column;
617
+ gap: 12px;
618
+ pointer-events: none;
619
+ }
620
+
621
+ .toast {
622
+ background: rgba(30, 41, 59, 0.9);
623
+ backdrop-filter: blur(12px);
624
+ border: 1px solid var(--panel-border);
625
+ border-radius: 8px;
626
+ padding: 14px 20px;
627
+ color: #fff;
628
+ font-size: 14px;
629
+ min-width: 280px;
630
+ box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.3);
631
+ display: flex;
632
+ align-items: center;
633
+ gap: 10px;
634
+ transform: translateX(120%);
635
+ transition: transform 0.3s cubic-bezier(0.68, -0.55, 0.265, 1.55), opacity 0.3s;
636
+ pointer-events: auto;
637
+ opacity: 0;
638
+ }
639
+
640
+ .toast.show {
641
+ transform: translateX(0);
642
+ opacity: 1;
643
+ }
644
+
645
+ .toast-success { border-left: 4px solid var(--success); }
646
+ .toast-error { border-left: 4px solid var(--danger); }
647
+ .toast-info { border-left: 4px solid var(--primary); }
648
+
649
+ .toast-icon {
650
+ font-size: 16px;
651
+ flex-shrink: 0;
652
+ }
653
+
654
+ /* 打字指示器 (Typing Loader) */
655
+ .typing-indicator {
656
+ display: flex;
657
+ align-items: center;
658
+ gap: 6px;
659
+ padding: 8px 12px;
660
+ }
661
+
662
+ .typing-dot {
663
+ width: 8px;
664
+ height: 8px;
665
+ background-color: var(--text-muted);
666
+ border-radius: 50%;
667
+ animation: typing-bounce 1.4s infinite ease-in-out both;
668
+ }
669
+
670
+ .typing-dot:nth-child(1) { animation-delay: -0.32s; }
671
+ .typing-dot:nth-child(2) { animation-delay: -0.16s; }
672
+
673
+ @keyframes typing-bounce {
674
+ 0%, 80%, 100% { transform: scale(0); }
675
+ 40% { transform: scale(1.0); }
676
+ /* Markdown 样式修饰 */
677
+ .message p {
678
+ margin-bottom: 8px;
679
+ }
680
+ .message p:last-child {
681
+ margin-bottom: 0;
682
+ }
683
+
684
+ .message ul, .message ol {
685
+ margin-bottom: 12px;
686
+ padding-left: 20px;
687
+ }
688
+
689
+ .message li {
690
+ margin-bottom: 4px;
691
+ }
692
+
693
+ .message code {
694
+ font-family: 'Fira Code', monospace;
695
+ background: rgba(0, 0, 0, 0.25);
696
+ padding: 2px 6px;
697
+ border-radius: 4px;
698
+ font-size: 13px;
699
+ }
700
+
701
+ body.light-mode .message code {
702
+ background: rgba(0, 0, 0, 0.06);
703
+ }
704
+
705
+ .message pre {
706
+ margin: 12px 0;
707
+ border-radius: 8px;
708
+ overflow: hidden;
709
+ border: 1px solid var(--panel-border);
710
+ }
711
+
712
+ .message pre code {
713
+ display: block;
714
+ padding: 14px 18px;
715
+ overflow-x: auto;
716
+ background: rgba(15, 23, 42, 0.5) !important;
717
+ font-size: 13px;
718
+ line-height: 1.5;
719
+ color: #f8fafc;
720
+ }
721
+
722
+ body.light-mode .message pre code {
723
+ background: rgba(248, 250, 252, 0.95) !important;
724
+ color: #1e293b;
725
+ }
726
+
727
+ .message h1, .message h2, .message h3, .message h4 {
728
+ margin-top: 16px;
729
+ margin-bottom: 8px;
730
+ font-weight: 600;
731
+ }
732
+ .message h1:first-child, .message h2:first-child, .message h3:first-child {
733
+ margin-top: 0;
734
+ }
735
+ </style>
736
+ </head>
737
+ <body>
738
+
739
+ <!-- 鉴权遮罩 -->
740
+ <div id="auth-overlay" style="display: none;">
741
+ <div class="auth-card">
742
+ <h2>鉴权验证</h2>
743
+ <p>请输入 CCLI Server 的访问 Token 进行连接</p>
744
+ <div class="auth-input-group">
745
+ <label for="token-input">访问 Token</label>
746
+ <input type="password" id="token-input" placeholder="输入 token...">
747
+ </div>
748
+ <button class="btn" onclick="saveTokenAndReload()">连接</button>
749
+ </div>
750
+ </div>
751
+
752
+ <!-- 侧边栏 -->
753
+ <aside>
754
+ <div class="sidebar-header">
755
+ <div class="logo">CCLI Console <span>Web</span></div>
756
+ </div>
757
+ <div class="sidebar-actions">
758
+ <button class="btn btn-new-session" onclick="createNewSession()">
759
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"></line><line x1="5" y1="12" x2="19" y2="12"></line></svg>
760
+ 新建会话
761
+ </button>
762
+ </div>
763
+ <div class="session-list-container" id="session-list">
764
+ <!-- 会话列表 -->
765
+ </div>
766
+ <div class="sidebar-footer">
767
+ <span style="font-size: 12px; color: var(--text-muted);" id="connection-status">未连接</span>
768
+ <div style="display: flex; gap: 12px; align-items: center;">
769
+ <button onclick="toggleTheme()" id="theme-toggle-btn" title="切换日夜间模式">☀️ 日间</button>
770
+ <button onclick="logout()">退出</button>
771
+ </div>
772
+ </div>
773
+ </aside>
774
+
775
+ <!-- 主对话区 -->
776
+ <main>
777
+ <header>
778
+ <div class="header-info">
779
+ <h1 id="active-session-title">请选择或新建会话</h1>
780
+ <p id="active-session-subtitle">CCLI server-sent events stream</p>
781
+ </div>
782
+ </header>
783
+
784
+ <div class="chat-area" id="chat-area" style="display: none;"></div>
785
+
786
+ <div class="empty-state" id="empty-state">
787
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
788
+ <path stroke-linecap="round" stroke-linejoin="round" d="M8.625 12a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H8.25m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0H12m4.125 0a.375.375 0 11-.75 0 .375.375 0 01.75 0zm0 0h-.375M21 12c0 4.556-4.03 8.25-9 8.25a9.764 9.764 0 01-2.555-.337A5.972 5.972 0 015.41 20.97a5.969 5.969 0 01-.474-.065 4.48 4.48 0 00.978-2.025c.09-.457-.133-.901-.467-1.226C3.93 16.178 3 14.189 3 12c0-4.556 4.03-8.25 9-8.25s9 3.694 9 8.25z" />
789
+ </svg>
790
+ <h2>开启多轮对话</h2>
791
+ <p>从左侧选择会话或者点击新建会话。在这里,AI 的工具执行结果会以结构化卡片展示,当触发敏感操作时会请求您的审批确认。</p>
792
+ <button class="btn" style="width: auto; margin-top: 12px; padding: 10px 24px;" onclick="createNewSession()">
793
+ 🚀 开启新会话
794
+ </button>
795
+ </div>
796
+
797
+ <div class="input-area-container" id="input-area-container" style="display: none;">
798
+ <div class="input-box">
799
+ <textarea id="prompt-input" placeholder="输入消息(Enter 发送,Shift+Enter 换行)..." onkeydown="handleInputKeydown(event)"></textarea>
800
+ <button class="btn-send" onclick="sendPrompt()">
801
+ <svg viewBox="0 0 24 24"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z"/></svg>
802
+ </button>
803
+ </div>
804
+ </div>
805
+ </main>
806
+
807
+ <script>
808
+ let currentSessionId = null;
809
+ let token = null;
810
+ let eventSource = null;
811
+ let pendingApproval = null;
812
+ let assistantMessageEl = null;
813
+ let assistantText = '';
814
+
815
+ function showToast(message, type = 'info') {
816
+ let container = document.getElementById('toast-container');
817
+ if (!container) {
818
+ container = document.createElement('div');
819
+ container.id = 'toast-container';
820
+ container.className = 'toast-container';
821
+ document.body.appendChild(container);
822
+ }
823
+
824
+ const toast = document.createElement('div');
825
+ toast.className = `toast toast-${type}`;
826
+
827
+ let icon = 'ℹ️';
828
+ if (type === 'success') icon = '✅';
829
+ if (type === 'error') icon = '❌';
830
+
831
+ toast.innerHTML = `
832
+ <span class="toast-icon">${icon}</span>
833
+ <span class="toast-message">${escapeHtml(message)}</span>
834
+ `;
835
+
836
+ container.appendChild(toast);
837
+
838
+ setTimeout(() => toast.classList.add('show'), 10);
839
+
840
+ setTimeout(() => {
841
+ toast.classList.remove('show');
842
+ setTimeout(() => toast.remove(), 300);
843
+ }, 3000);
844
+ }
845
+
846
+ function toggleTheme() {
847
+ const isLight = document.body.classList.toggle('light-mode');
848
+ const btn = document.getElementById('theme-toggle-btn');
849
+ const hlTheme = document.getElementById('hl-theme');
850
+ if (isLight) {
851
+ btn.innerText = '🌙 夜间';
852
+ localStorage.setItem('ccli_theme', 'light');
853
+ if (hlTheme) hlTheme.href = 'https://cdn.jsdelivr.net/npm/highlight.js@11.9.0/styles/github.min.css';
854
+ } else {
855
+ btn.innerText = '☀️ 日间';
856
+ localStorage.setItem('ccli_theme', 'dark');
857
+ if (hlTheme) hlTheme.href = 'https://cdn.jsdelivr.net/npm/highlight.js@11.9.0/styles/github-dark.min.css';
858
+ }
859
+ }
860
+
861
+ function initTheme() {
862
+ const savedTheme = localStorage.getItem('ccli_theme') || 'dark';
863
+ const btn = document.getElementById('theme-toggle-btn');
864
+ const hlTheme = document.getElementById('hl-theme');
865
+ if (savedTheme === 'light') {
866
+ document.body.classList.add('light-mode');
867
+ if (btn) btn.innerText = '🌙 夜间';
868
+ if (hlTheme) hlTheme.href = 'https://cdn.jsdelivr.net/npm/highlight.js@11.9.0/styles/github.min.css';
869
+ } else {
870
+ document.body.classList.remove('light-mode');
871
+ if (btn) btn.innerText = '☀️ 日间';
872
+ if (hlTheme) hlTheme.href = 'https://cdn.jsdelivr.net/npm/highlight.js@11.9.0/styles/github-dark.min.css';
873
+ }
874
+ }
875
+
876
+ // 初始化鉴权
877
+ function initAuth() {
878
+ initTheme();
879
+ // 1. 尝试从 URL 拿 token
880
+ const urlParams = new URLSearchParams(window.location.search);
881
+ const urlToken = urlParams.get('token');
882
+ if (urlToken) {
883
+ token = urlToken;
884
+ localStorage.setItem('ccli_token', token);
885
+ // 去掉 URL 中的 token 参数,保持干净
886
+ window.history.replaceState({}, document.title, window.location.pathname);
887
+ } else {
888
+ // 2. 尝试从 localStorage 拿 token
889
+ token = localStorage.getItem('ccli_token');
890
+ }
891
+
892
+ if (!token) {
893
+ document.getElementById('auth-overlay').style.display = 'flex';
894
+ } else {
895
+ document.getElementById('auth-overlay').style.display = 'none';
896
+ loadSessions();
897
+ }
898
+ }
899
+
900
+ function saveTokenAndReload() {
901
+ const val = document.getElementById('token-input').value.trim();
902
+ if (val) {
903
+ token = val;
904
+ localStorage.setItem('ccli_token', token);
905
+ document.getElementById('auth-overlay').style.display = 'none';
906
+ loadSessions();
907
+ }
908
+ }
909
+
910
+ function logout() {
911
+ localStorage.removeItem('ccli_token');
912
+ if (eventSource) {
913
+ eventSource.close();
914
+ }
915
+ token = null;
916
+ currentSessionId = null;
917
+ document.getElementById('session-list').innerHTML = '';
918
+ const chatArea = document.getElementById('chat-area');
919
+ chatArea.innerHTML = '';
920
+ chatArea.style.display = 'none';
921
+ document.getElementById('input-area-container').style.display = 'none';
922
+ document.getElementById('empty-state').style.display = 'flex';
923
+ document.getElementById('active-session-title').innerText = '请选择或新建会话';
924
+ document.getElementById('connection-status').innerText = '未连接';
925
+ initAuth();
926
+ }
927
+
928
+ // 调用 API 的通用包装
929
+ async function apiFetch(path, options = {}) {
930
+ if (!options.headers) options.headers = {};
931
+ options.headers['Authorization'] = 'Bearer ' + token;
932
+
933
+ try {
934
+ const res = await fetch(path, options);
935
+ if (res.status === 401) {
936
+ logout();
937
+ throw new Error('Unauthorized');
938
+ }
939
+ return res;
940
+ } catch (e) {
941
+ console.error('API Error:', e);
942
+ throw e;
943
+ }
944
+ }
945
+
946
+ function getSessionMeta(id) {
947
+ let meta = {};
948
+ try {
949
+ meta = JSON.parse(localStorage.getItem('ccli_session_meta') || '{}');
950
+ } catch (e) {}
951
+ if (!meta[id]) {
952
+ meta[id] = {
953
+ title: '新会话',
954
+ time: new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })
955
+ };
956
+ localStorage.setItem('ccli_session_meta', JSON.stringify(meta));
957
+ }
958
+ return meta[id];
959
+ }
960
+
961
+ function updateSessionMeta(id, updates) {
962
+ let meta = {};
963
+ try {
964
+ meta = JSON.parse(localStorage.getItem('ccli_session_meta') || '{}');
965
+ } catch (e) {}
966
+ meta[id] = { ...(meta[id] || { title: '新会话', time: new Date().toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' }) }), ...updates };
967
+ localStorage.setItem('ccli_session_meta', JSON.stringify(meta));
968
+ }
969
+
970
+ // 获取会话列表
971
+ async function loadSessions() {
972
+ try {
973
+ const res = await apiFetch('/sessions');
974
+ const data = await res.json();
975
+ const container = document.getElementById('session-list');
976
+ container.innerHTML = '';
977
+
978
+ if (data.sessions && data.sessions.length > 0) {
979
+ data.sessions.forEach(id => {
980
+ const item = document.createElement('div');
981
+ item.className = 'session-item' + (id === currentSessionId ? ' active' : '');
982
+ item.setAttribute('data-id', id);
983
+ item.onclick = () => selectSession(id);
984
+
985
+ const meta = getSessionMeta(id);
986
+
987
+ const itemContent = document.createElement('div');
988
+ itemContent.style.display = 'flex';
989
+ itemContent.style.flexDirection = 'column';
990
+ itemContent.style.gap = '2px';
991
+ itemContent.style.overflow = 'hidden';
992
+
993
+ const nameSpan = document.createElement('span');
994
+ nameSpan.className = 'session-item-name';
995
+ nameSpan.style.fontWeight = '500';
996
+ nameSpan.innerText = meta.title;
997
+
998
+ const timeSpan = document.createElement('span');
999
+ timeSpan.style.fontSize = '11px';
1000
+ timeSpan.style.color = 'var(--text-muted)';
1001
+ timeSpan.innerText = meta.time;
1002
+
1003
+ itemContent.appendChild(nameSpan);
1004
+ itemContent.appendChild(timeSpan);
1005
+
1006
+ item.appendChild(itemContent);
1007
+ container.appendChild(item);
1008
+ });
1009
+ }
1010
+ document.getElementById('connection-status').innerText = '已连接';
1011
+ } catch (e) {
1012
+ document.getElementById('connection-status').innerText = '连接失败';
1013
+ }
1014
+ }
1015
+
1016
+ // 创建新会话
1017
+ async function createNewSession() {
1018
+ try {
1019
+ const res = await apiFetch('/sessions', { method: 'POST' });
1020
+ const data = await res.json();
1021
+ if (data.ok) {
1022
+ currentSessionId = data.id;
1023
+ await loadSessions();
1024
+ selectSession(data.id);
1025
+ }
1026
+ } catch (e) {
1027
+ showToast('创建会话失败:' + e.message, 'error');
1028
+ }
1029
+ }
1030
+
1031
+ // 选中某个会话
1032
+ function selectSession(id) {
1033
+ currentSessionId = id;
1034
+
1035
+ // 切换列表 active 态
1036
+ document.querySelectorAll('.session-item').forEach(item => {
1037
+ if (item.getAttribute('data-id') === id) {
1038
+ item.classList.add('active');
1039
+ } else {
1040
+ item.classList.remove('active');
1041
+ }
1042
+ });
1043
+
1044
+ const meta = getSessionMeta(id);
1045
+ document.getElementById('active-session-title').innerText = meta.title;
1046
+ document.getElementById('active-session-subtitle').innerText = '会话 ID: ' + id;
1047
+ document.getElementById('empty-state').style.display = 'none';
1048
+ document.getElementById('input-area-container').style.display = 'block';
1049
+
1050
+ // 重置聊天区
1051
+ const chatArea = document.getElementById('chat-area');
1052
+ chatArea.style.display = 'flex';
1053
+ chatArea.innerHTML = '';
1054
+
1055
+ // 建立新的 SSE 实时连接
1056
+ connectStream(id);
1057
+ }
1058
+
1059
+ // 渲染 Markdown 并进行代码块高亮
1060
+ function renderMarkdownToElement(el, markdownText) {
1061
+ if (typeof marked !== 'undefined') {
1062
+ el.innerHTML = marked.parse(markdownText);
1063
+ // 高亮代码块
1064
+ if (typeof hljs !== 'undefined') {
1065
+ el.querySelectorAll('pre code').forEach((block) => {
1066
+ hljs.highlightElement(block);
1067
+ });
1068
+ }
1069
+ } else {
1070
+ // 兜底降级渲染
1071
+ el.textContent = markdownText;
1072
+ }
1073
+ }
1074
+
1075
+ // 连接会话 SSE 事件流
1076
+ function connectStream(id) {
1077
+ if (eventSource) {
1078
+ eventSource.close();
1079
+ }
1080
+
1081
+ // SSE query param 携带 token
1082
+ eventSource = new EventSource(`/sessions/${id}/stream?token=${encodeURIComponent(token)}`);
1083
+
1084
+ eventSource.addEventListener('ready', (e) => {
1085
+ console.log('SSE Stream Ready');
1086
+ });
1087
+
1088
+ eventSource.addEventListener('text_delta', (e) => {
1089
+ const data = JSON.parse(e.data);
1090
+ if (!assistantMessageEl) {
1091
+ assistantMessageEl = createMessageBubble('assistant', '');
1092
+ assistantText = '';
1093
+ }
1094
+ const msgTextEl = assistantMessageEl.querySelector('.message');
1095
+ const loader = msgTextEl.querySelector('#assistant-typing');
1096
+ if (loader) {
1097
+ msgTextEl.innerHTML = '';
1098
+ }
1099
+ assistantText += data.text;
1100
+ renderMarkdownToElement(msgTextEl, assistantText);
1101
+ scrollToBottom();
1102
+ });
1103
+
1104
+ eventSource.addEventListener('tool_call', (e) => {
1105
+ const data = JSON.parse(e.data);
1106
+ // 每次工具调用,建一个 collapsible 卡片
1107
+ createToolCard(data.toolCall);
1108
+ scrollToBottom();
1109
+ });
1110
+
1111
+ eventSource.addEventListener('tool_result', (e) => {
1112
+ const data = JSON.parse(e.data);
1113
+ // 更新已有的工具卡片结果
1114
+ updateToolCardResult(data.toolCall.id, data.result);
1115
+ scrollToBottom();
1116
+ });
1117
+
1118
+ eventSource.addEventListener('permission_request', (e) => {
1119
+ const data = JSON.parse(e.data);
1120
+ // 弹出审批面板
1121
+ showApprovalCard(data);
1122
+ scrollToBottom();
1123
+ });
1124
+
1125
+ eventSource.addEventListener('done', (e) => {
1126
+ console.log('Turn Done');
1127
+ if (assistantMessageEl) {
1128
+ const msgTextEl = assistantMessageEl.querySelector('.message');
1129
+ const loader = msgTextEl.querySelector('#assistant-typing');
1130
+ if (loader) {
1131
+ loader.remove();
1132
+ if (!msgTextEl.textContent.trim()) {
1133
+ assistantMessageEl.remove();
1134
+ }
1135
+ }
1136
+ }
1137
+ assistantMessageEl = null; // 重置,准备下一轮
1138
+ assistantText = '';
1139
+ });
1140
+
1141
+ eventSource.addEventListener('error', (e) => {
1142
+ console.error('SSE Error:', e);
1143
+ });
1144
+ }
1145
+
1146
+ // 创建消息气泡
1147
+ function createMessageBubble(role, text) {
1148
+ const chatArea = document.getElementById('chat-area');
1149
+
1150
+ const row = document.createElement('div');
1151
+ row.className = 'message-row ' + role;
1152
+
1153
+ const bubble = document.createElement('div');
1154
+ bubble.className = 'message';
1155
+ bubble.textContent = text;
1156
+
1157
+ row.appendChild(bubble);
1158
+ chatArea.appendChild(row);
1159
+ return row;
1160
+ }
1161
+
1162
+ // 创建工具调用卡片
1163
+ function createToolCard(toolCall) {
1164
+ const chatArea = document.getElementById('chat-area');
1165
+
1166
+ const card = document.createElement('div');
1167
+ card.className = 'tool-card';
1168
+ card.setAttribute('id', 'tool-' + toolCall.id);
1169
+
1170
+ card.innerHTML = `
1171
+ <div class="tool-header" onclick="toggleCardBody('${toolCall.id}')">
1172
+ <div class="tool-header-info">
1173
+ <span class="tool-icon">⚙</span>
1174
+ <span>调用工具 <span class="tool-name">${toolCall.name}</span></span>
1175
+ </div>
1176
+ <span style="font-size: 11px; opacity: 0.6;">展开/折叠</span>
1177
+ </div>
1178
+ <div class="tool-body" id="body-${toolCall.id}">${escapeHtml(toolCall.arguments)}</div>
1179
+ `;
1180
+
1181
+ chatArea.appendChild(card);
1182
+ }
1183
+
1184
+ function toggleCardBody(id) {
1185
+ const body = document.getElementById('body-' + id);
1186
+ if (body) {
1187
+ body.style.display = body.style.display === 'none' ? 'block' : 'none';
1188
+ }
1189
+ }
1190
+
1191
+ // 更新工具调用结果
1192
+ function updateToolCardResult(id, result) {
1193
+ const card = document.getElementById('tool-' + id);
1194
+ if (!card) return;
1195
+
1196
+ const resHeader = document.createElement('div');
1197
+ resHeader.className = 'tool-result-header';
1198
+ resHeader.innerText = '执行结果:';
1199
+
1200
+ const resBody = document.createElement('div');
1201
+ resBody.className = 'tool-result-body' + (result.isError ? ' error' : '');
1202
+ resBody.innerText = result.output;
1203
+
1204
+ card.appendChild(resHeader);
1205
+ card.appendChild(resBody);
1206
+ }
1207
+
1208
+ // 显示审批回路面板
1209
+ function showApprovalCard(data) {
1210
+ const chatArea = document.getElementById('chat-area');
1211
+
1212
+ const card = document.createElement('div');
1213
+ card.className = 'approval-card';
1214
+ card.setAttribute('id', 'approval-' + data.approvalId);
1215
+
1216
+ card.innerHTML = `
1217
+ <div class="approval-header">
1218
+ <span>⚠ 审批请求:正在调用敏感工具 [${data.tool}]</span>
1219
+ </div>
1220
+ <div class="approval-preview">${escapeHtml(data.preview)}</div>
1221
+ <div class="approval-actions">
1222
+ <button class="btn btn-approve" onclick="submitApproval('${data.approvalId}', 'yes')">确认执行</button>
1223
+ <button class="btn btn-always" onclick="submitApproval('${data.approvalId}', 'always')">始终允许</button>
1224
+ <button class="btn btn-deny" onclick="submitApproval('${data.approvalId}', 'no')">拒绝</button>
1225
+ </div>
1226
+ `;
1227
+
1228
+ chatArea.appendChild(card);
1229
+ }
1230
+
1231
+ // 回执审批决定
1232
+ async function submitApproval(approvalId, choice) {
1233
+ const card = document.getElementById('approval-' + approvalId);
1234
+ if (card) {
1235
+ card.remove(); // 移除审批卡片
1236
+ }
1237
+
1238
+ try {
1239
+ await apiFetch(`/sessions/${currentSessionId}/approvals`, {
1240
+ method: 'POST',
1241
+ headers: { 'Content-Type': 'application/json' },
1242
+ body: JSON.stringify({ approvalId, choice })
1243
+ });
1244
+ } catch (e) {
1245
+ showToast('提交审批失败:' + e.message, 'error');
1246
+ }
1247
+ }
1248
+
1249
+ // 发送消息
1250
+ async function sendPrompt() {
1251
+ if (!currentSessionId) {
1252
+ showToast('请先选择或新建一个会话', 'info');
1253
+ return;
1254
+ }
1255
+
1256
+ const textarea = document.getElementById('prompt-input');
1257
+ const prompt = textarea.value.trim();
1258
+ if (!prompt) return;
1259
+
1260
+ textarea.value = '';
1261
+ textarea.style.height = '24px';
1262
+
1263
+ // 本地直接渲染 User 消息
1264
+ createMessageBubble('user', prompt);
1265
+ scrollToBottom();
1266
+
1267
+ // 渲染 Assistant 的打字等待状态
1268
+ const bubble = createMessageBubble('assistant', '');
1269
+ assistantMessageEl = bubble;
1270
+ const msgTextEl = bubble.querySelector('.message');
1271
+ msgTextEl.innerHTML = `
1272
+ <div class="typing-indicator" id="assistant-typing">
1273
+ <div class="typing-dot"></div>
1274
+ <div class="typing-dot"></div>
1275
+ <div class="typing-dot"></div>
1276
+ </div>
1277
+ `;
1278
+ scrollToBottom();
1279
+
1280
+ // 如果是初始的“新会话”,用第一条 prompt 作为会话标题并更新
1281
+ const meta = getSessionMeta(currentSessionId);
1282
+ if (meta.title === '新会话') {
1283
+ const newTitle = prompt.length > 15 ? prompt.slice(0, 15) + '...' : prompt;
1284
+ updateSessionMeta(currentSessionId, { title: newTitle });
1285
+ await loadSessions();
1286
+
1287
+ // 实时更新头部标题
1288
+ document.getElementById('active-session-title').innerText = newTitle;
1289
+ }
1290
+
1291
+ try {
1292
+ await apiFetch(`/sessions/${currentSessionId}/prompts`, {
1293
+ method: 'POST',
1294
+ headers: { 'Content-Type': 'application/json' },
1295
+ body: JSON.stringify({ prompt })
1296
+ });
1297
+ } catch (e) {
1298
+ if (assistantMessageEl) {
1299
+ assistantMessageEl.remove();
1300
+ assistantMessageEl = null;
1301
+ }
1302
+ showToast('发送失败,请确认服务状态。', 'error');
1303
+ }
1304
+ }
1305
+
1306
+ // 常用辅助功能
1307
+ function handleInputKeydown(e) {
1308
+ if (e.key === 'Enter' && !e.shiftKey) {
1309
+ e.preventDefault();
1310
+ sendPrompt();
1311
+ }
1312
+ }
1313
+
1314
+ // 绑定输入框自动高度
1315
+ const tx = document.getElementById('prompt-input');
1316
+ tx.addEventListener("input", function() {
1317
+ this.style.height = '24px';
1318
+ if (this.scrollHeight > 24) {
1319
+ this.style.height = this.scrollHeight + 'px';
1320
+ }
1321
+ }, false);
1322
+
1323
+ function scrollToBottom() {
1324
+ const chatArea = document.getElementById('chat-area');
1325
+ chatArea.scrollTop = chatArea.scrollHeight;
1326
+ }
1327
+
1328
+ function escapeHtml(str) {
1329
+ return str
1330
+ .replace(/&/g, "&amp;")
1331
+ .replace(/</g, "&lt;")
1332
+ .replace(/>/g, "&gt;")
1333
+ .replace(/"/g, "&quot;")
1334
+ .replace(/'/g, "&#039;");
1335
+ }
1336
+
1337
+ // 首次加载
1338
+ window.onload = initAuth;
1339
+ </script>
1340
+ </body>
1341
+ </html>