@greatlhd/ailo-desktop 1.0.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 (73) hide show
  1. package/copy-static.mjs +11 -0
  2. package/dist/browser_control.js +767 -0
  3. package/dist/browser_snapshot.js +174 -0
  4. package/dist/cli.js +36 -0
  5. package/dist/code_executor.js +95 -0
  6. package/dist/config_server.js +658 -0
  7. package/dist/connection_util.js +14 -0
  8. package/dist/constants.js +2 -0
  9. package/dist/desktop_state_store.js +57 -0
  10. package/dist/desktop_types.js +1 -0
  11. package/dist/desktop_verifier.js +40 -0
  12. package/dist/dingtalk-handler.js +173 -0
  13. package/dist/dingtalk-types.js +1 -0
  14. package/dist/email_handler.js +501 -0
  15. package/dist/exec_tool.js +90 -0
  16. package/dist/feishu-handler.js +620 -0
  17. package/dist/feishu-types.js +8 -0
  18. package/dist/feishu-utils.js +162 -0
  19. package/dist/fs_tools.js +398 -0
  20. package/dist/index.js +433 -0
  21. package/dist/mcp/config-manager.js +64 -0
  22. package/dist/mcp/index.js +3 -0
  23. package/dist/mcp/rpc.js +109 -0
  24. package/dist/mcp/session.js +140 -0
  25. package/dist/mcp_manager.js +253 -0
  26. package/dist/mouse_keyboard.js +516 -0
  27. package/dist/qq-handler.js +153 -0
  28. package/dist/qq-types.js +15 -0
  29. package/dist/qq-ws.js +178 -0
  30. package/dist/screenshot.js +271 -0
  31. package/dist/skills_hub.js +212 -0
  32. package/dist/skills_manager.js +103 -0
  33. package/dist/static/AGENTS.md +25 -0
  34. package/dist/static/app.css +539 -0
  35. package/dist/static/app.html +292 -0
  36. package/dist/static/app.js +380 -0
  37. package/dist/static/chat.html +994 -0
  38. package/dist/time_tool.js +22 -0
  39. package/dist/utils.js +15 -0
  40. package/package.json +38 -0
  41. package/src/browser_control.ts +739 -0
  42. package/src/browser_snapshot.ts +196 -0
  43. package/src/cli.ts +44 -0
  44. package/src/code_executor.ts +101 -0
  45. package/src/config_server.ts +723 -0
  46. package/src/connection_util.ts +23 -0
  47. package/src/constants.ts +2 -0
  48. package/src/desktop_state_store.ts +64 -0
  49. package/src/desktop_types.ts +44 -0
  50. package/src/desktop_verifier.ts +45 -0
  51. package/src/dingtalk-types.ts +26 -0
  52. package/src/exec_tool.ts +93 -0
  53. package/src/feishu-handler.ts +722 -0
  54. package/src/feishu-types.ts +66 -0
  55. package/src/feishu-utils.ts +174 -0
  56. package/src/fs_tools.ts +411 -0
  57. package/src/index.ts +474 -0
  58. package/src/mcp/config-manager.ts +85 -0
  59. package/src/mcp/index.ts +7 -0
  60. package/src/mcp/rpc.ts +131 -0
  61. package/src/mcp/session.ts +182 -0
  62. package/src/mcp_manager.ts +273 -0
  63. package/src/mouse_keyboard.ts +526 -0
  64. package/src/qq-types.ts +49 -0
  65. package/src/qq-ws.ts +223 -0
  66. package/src/screenshot.ts +297 -0
  67. package/src/static/app.css +539 -0
  68. package/src/static/app.html +292 -0
  69. package/src/static/app.js +380 -0
  70. package/src/static/chat.html +994 -0
  71. package/src/time_tool.ts +24 -0
  72. package/src/utils.ts +22 -0
  73. package/tsconfig.json +13 -0
@@ -0,0 +1,994 @@
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>Ailo 网页聊天</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=Noto+Sans+SC:wght@400;500;600;700&family=DM+Sans:ital,opsz,wght@0,9..40,400;0,9..40,500;0,9..40,600;1,9..40,400&display=swap" rel="stylesheet">
10
+ <style>
11
+ * {
12
+ box-sizing: border-box;
13
+ margin: 0;
14
+ padding: 0;
15
+ }
16
+
17
+ :root {
18
+ --bg-base: #0a0a0f;
19
+ --bg-elevated: #12121a;
20
+ --bg-surface: #18181f;
21
+ --bg-surface-2: #1e1e28;
22
+ --bg-muted: #262630;
23
+ --accent: #7c6ff7;
24
+ --accent-light: #9d8fff;
25
+ --accent-soft: rgba(124, 111, 247, 0.15);
26
+ --accent-user: #5b9eef;
27
+ --accent-user-soft: rgba(91, 158, 239, 0.15);
28
+ --text-primary: #f0f0f5;
29
+ --text-secondary: #9898a8;
30
+ --text-muted: #606070;
31
+ --border: rgba(255,255,255,0.08);
32
+ --border-focus: rgba(124, 111, 247, 0.5);
33
+ --success: #50d68d;
34
+ --error: #f57070;
35
+ --radius-sm: 8px;
36
+ --radius-md: 12px;
37
+ --radius-lg: 16px;
38
+ --radius-xl: 24px;
39
+ --shadow-sm: 0 2px 8px rgba(0,0,0,0.3);
40
+ --shadow-md: 32px rgba0 8px(0,0,0,0.4);
41
+ }
42
+
43
+ body {
44
+ font-family: 'DM Sans', 'Noto Sans SC', -apple-system, BlinkMacSystemFont, sans-serif;
45
+ background: var(--bg-base);
46
+ color: var(--text-primary);
47
+ height: 100vh;
48
+ display: flex;
49
+ flex-direction: column;
50
+ -webkit-font-smoothing: antialiased;
51
+ background-image:
52
+ radial-gradient(ellipse at 20% 0%, rgba(124, 111, 247, 0.08) 0%, transparent 50%),
53
+ radial-gradient(ellipse at 80% 100%, rgba(91, 158, 239, 0.06) 0%, transparent 50%);
54
+ }
55
+
56
+ /* Header */
57
+ header {
58
+ background: linear-gradient(180deg, var(--bg-elevated) 0%, rgba(18, 18, 26, 0.95) 100%);
59
+ padding: 16px 28px;
60
+ border-bottom: 1px solid var(--border);
61
+ display: flex;
62
+ align-items: center;
63
+ justify-content: space-between;
64
+ flex-shrink: 0;
65
+ backdrop-filter: blur(12px);
66
+ position: relative;
67
+ z-index: 10;
68
+ }
69
+
70
+ header::after {
71
+ content: '';
72
+ position: absolute;
73
+ bottom: 0;
74
+ left: 0;
75
+ right: 0;
76
+ height: 1px;
77
+ background: linear-gradient(90deg, transparent, rgba(124, 111, 247, 0.3), transparent);
78
+ }
79
+
80
+ .brand {
81
+ display: flex;
82
+ align-items: center;
83
+ gap: 12px;
84
+ }
85
+
86
+ .brand-icon {
87
+ width: 36px;
88
+ height: 36px;
89
+ background: linear-gradient(135deg, var(--accent), var(--accent-light));
90
+ border-radius: 10px;
91
+ display: flex;
92
+ align-items: center;
93
+ justify-content: center;
94
+ box-shadow: 0 4px 12px rgba(124, 111, 247, 0.3);
95
+ }
96
+
97
+ .brand-icon svg {
98
+ width: 20px;
99
+ height: 20px;
100
+ color: white;
101
+ }
102
+
103
+ h1 {
104
+ font-size: 18px;
105
+ font-weight: 700;
106
+ letter-spacing: -0.02em;
107
+ background: linear-gradient(135deg, var(--text-primary), var(--accent-light));
108
+ -webkit-background-clip: text;
109
+ -webkit-text-fill-color: transparent;
110
+ background-clip: text;
111
+ }
112
+
113
+ .header-right {
114
+ display: flex;
115
+ align-items: center;
116
+ gap: 20px;
117
+ }
118
+
119
+ .header-actions {
120
+ display: flex;
121
+ align-items: center;
122
+ gap: 10px;
123
+ }
124
+
125
+ .status {
126
+ font-size: 13px;
127
+ color: var(--text-secondary);
128
+ display: flex;
129
+ align-items: center;
130
+ gap: 10px;
131
+ padding: 8px 16px;
132
+ background: var(--bg-surface);
133
+ border-radius: 100px;
134
+ border: 1px solid var(--border);
135
+ }
136
+
137
+ .status-dot {
138
+ width: 8px;
139
+ height: 8px;
140
+ border-radius: 50%;
141
+ background: var(--text-muted);
142
+ transition: all 0.3s ease;
143
+ }
144
+
145
+ .status-dot.connected {
146
+ background: var(--success);
147
+ box-shadow: 0 0 12px rgba(80, 214, 141, 0.5);
148
+ }
149
+
150
+ .status-dot.disconnected {
151
+ background: var(--error);
152
+ box-shadow: 0 0 12px rgba(245, 112, 112, 0.5);
153
+ }
154
+
155
+ .status-dot.connecting {
156
+ background: #f5a542;
157
+ animation: pulse 1.5s infinite;
158
+ }
159
+
160
+ @keyframes pulse {
161
+ 0%, 100% { opacity: 1; transform: scale(1); }
162
+ 50% { opacity: 0.6; transform: scale(0.9); }
163
+ }
164
+
165
+ .user-badge {
166
+ display: inline-flex;
167
+ align-items: center;
168
+ gap: 10px;
169
+ background: linear-gradient(135deg, var(--bg-muted), var(--bg-surface-2));
170
+ padding: 8px 16px;
171
+ border-radius: 100px;
172
+ font-size: 13px;
173
+ font-weight: 500;
174
+ color: var(--text-primary);
175
+ border: 1px solid var(--border);
176
+ box-shadow: var(--shadow-sm);
177
+ }
178
+
179
+ .user-badge svg {
180
+ width: 16px;
181
+ height: 16px;
182
+ color: var(--accent);
183
+ }
184
+
185
+ .clear-history-btn {
186
+ display: inline-flex;
187
+ align-items: center;
188
+ gap: 8px;
189
+ border: 1px solid var(--border);
190
+ background: var(--bg-surface);
191
+ color: var(--text-secondary);
192
+ border-radius: 999px;
193
+ padding: 8px 14px;
194
+ font-size: 13px;
195
+ font-weight: 500;
196
+ cursor: pointer;
197
+ transition: all 0.2s ease;
198
+ }
199
+
200
+ .clear-history-btn:hover:not(:disabled) {
201
+ border-color: rgba(245, 112, 112, 0.45);
202
+ color: #f8a7a7;
203
+ background: rgba(245, 112, 112, 0.1);
204
+ }
205
+
206
+ .clear-history-btn:disabled {
207
+ opacity: 0.5;
208
+ cursor: not-allowed;
209
+ }
210
+
211
+ /* Chat Container */
212
+ #chat-container {
213
+ flex: 1;
214
+ overflow-y: auto;
215
+ padding: 28px;
216
+ display: flex;
217
+ flex-direction: column;
218
+ gap: 4px;
219
+ max-width: none;
220
+ margin: 0;
221
+ width: 100%;
222
+ position: relative;
223
+ }
224
+
225
+ #chat-container::-webkit-scrollbar {
226
+ width: 6px;
227
+ }
228
+
229
+ #chat-container::-webkit-scrollbar-track {
230
+ background: transparent;
231
+ }
232
+
233
+ #chat-container::-webkit-scrollbar-thumb {
234
+ background: var(--bg-muted);
235
+ border-radius: 3px;
236
+ }
237
+
238
+ #chat-container::-webkit-scrollbar-thumb:hover {
239
+ background: var(--text-muted);
240
+ }
241
+
242
+ /* Messages */
243
+ .message {
244
+ display: flex;
245
+ gap: 12px;
246
+ max-width: 85%;
247
+ animation: messageIn 0.3s ease;
248
+ }
249
+
250
+ @keyframes messageIn {
251
+ from {
252
+ opacity: 0;
253
+ transform: translateY(10px);
254
+ }
255
+ to {
256
+ opacity: 1;
257
+ transform: translateY(0);
258
+ }
259
+ }
260
+
261
+ .message.user {
262
+ align-self: flex-end;
263
+ flex-direction: row-reverse;
264
+ }
265
+
266
+ .message-avatar {
267
+ width: 36px;
268
+ height: 36px;
269
+ border-radius: 50%;
270
+ flex-shrink: 0;
271
+ display: flex;
272
+ align-items: center;
273
+ justify-content: center;
274
+ font-size: 14px;
275
+ font-weight: 600;
276
+ }
277
+
278
+ .message.user .message-avatar {
279
+ background: linear-gradient(135deg, var(--accent-user), #7ab3f5);
280
+ color: white;
281
+ }
282
+
283
+ .message.ailo .message-avatar {
284
+ background: linear-gradient(135deg, var(--accent), var(--accent-light));
285
+ color: white;
286
+ }
287
+
288
+ .message-content {
289
+ display: flex;
290
+ flex-direction: column;
291
+ gap: 4px;
292
+ }
293
+
294
+ .message.user .message-content {
295
+ align-items: flex-end;
296
+ }
297
+
298
+ .message .sender {
299
+ font-size: 12px;
300
+ font-weight: 500;
301
+ color: var(--text-muted);
302
+ padding: 0 4px;
303
+ }
304
+
305
+ .message-bubble {
306
+ padding: 14px 20px;
307
+ border-radius: var(--radius-lg);
308
+ line-height: 1.6;
309
+ white-space: pre-wrap;
310
+ word-break: break-word;
311
+ font-size: 15px;
312
+ position: relative;
313
+ }
314
+
315
+ .message.user .message-bubble {
316
+ background: linear-gradient(135deg, var(--accent-user) 0%, #6a8fdd 100%);
317
+ color: white;
318
+ border-bottom-right-radius: 6px;
319
+ box-shadow: 0 4px 16px rgba(91, 158, 239, 0.3);
320
+ }
321
+
322
+ .message.ailo .message-bubble {
323
+ background: var(--bg-surface);
324
+ border: 1px solid var(--border);
325
+ color: var(--text-primary);
326
+ border-bottom-left-radius: 6px;
327
+ box-shadow: var(--shadow-sm);
328
+ }
329
+
330
+ .message-time {
331
+ font-size: 11px;
332
+ color: var(--text-muted);
333
+ padding: 0 4px;
334
+ opacity: 0.7;
335
+
336
+ /* ─── 附件渲染 ────────────────────────────────── */
337
+ .chat-img {
338
+ display: block;
339
+ max-width: 240px;
340
+ max-height: 320px;
341
+ border-radius: 10px;
342
+ cursor: zoom-in;
343
+ margin-top: 6px;
344
+ border: 1px solid var(--border);
345
+ object-fit: cover;
346
+ }
347
+
348
+ .chat-file {
349
+ display: inline-flex;
350
+ align-items: center;
351
+ gap: 8px;
352
+ margin-top: 8px;
353
+ padding: 8px 14px;
354
+ background: var(--bg-muted);
355
+ border: 1px solid var(--border);
356
+ border-radius: 10px;
357
+ color: var(--text-primary);
358
+ text-decoration: none;
359
+ font-size: 14px;
360
+ transition: background .15s;
361
+ }
362
+ .chat-file:hover { background: var(--bg-surface-2); }
363
+ .chat-file-icon { font-size: 18px; flex-shrink: 0; }
364
+ .chat-file-name { word-break: break-all; }
365
+ }
366
+
367
+ /* Empty State */
368
+ .empty-state {
369
+ flex: 1;
370
+ display: flex;
371
+ flex-direction: column;
372
+ align-items: center;
373
+ justify-content: center;
374
+ color: var(--text-muted);
375
+ gap: 20px;
376
+ padding: 60px 20px;
377
+ }
378
+
379
+ .empty-state-icon {
380
+ width: 100px;
381
+ height: 100px;
382
+ background: var(--bg-surface);
383
+ border-radius: 50%;
384
+ display: flex;
385
+ align-items: center;
386
+ justify-content: center;
387
+ border: 1px solid var(--border);
388
+ position: relative;
389
+ }
390
+
391
+ .empty-state-icon::after {
392
+ content: '';
393
+ position: absolute;
394
+ inset: -4px;
395
+ border-radius: 50%;
396
+ background: linear-gradient(135deg, var(--accent), transparent);
397
+ opacity: 0.1;
398
+ z-index: -1;
399
+ }
400
+
401
+ .empty-state-icon svg {
402
+ width: 44px;
403
+ height: 44px;
404
+ color: var(--text-muted);
405
+ opacity: 0.6;
406
+ }
407
+
408
+ .empty-state p {
409
+ font-size: 16px;
410
+ color: var(--text-secondary);
411
+ }
412
+
413
+ .empty-state .hint {
414
+ font-size: 13px;
415
+ color: var(--text-muted);
416
+ margin-top: 8px;
417
+ }
418
+
419
+ /* Input Container */
420
+ #input-container {
421
+ background: linear-gradient(180deg, rgba(18, 18, 26, 0.98), var(--bg-elevated));
422
+ padding: 20px 28px 24px;
423
+ border-top: 1px solid var(--border);
424
+ display: flex;
425
+ gap: 14px;
426
+ align-items: flex-end;
427
+ backdrop-filter: blur(12px);
428
+ position: relative;
429
+ }
430
+
431
+ #input-container::before {
432
+ content: '';
433
+ position: absolute;
434
+ top: 0;
435
+ left: 20%;
436
+ right: 20%;
437
+ height: 1px;
438
+ background: linear-gradient(90deg, transparent, rgba(124, 111, 247, 0.2), transparent);
439
+ }
440
+
441
+ #message-input {
442
+ flex: 1;
443
+ background: var(--bg-surface);
444
+ border: 1px solid var(--border);
445
+ border-radius: var(--radius-lg);
446
+ padding: 16px 20px;
447
+ color: var(--text-primary);
448
+ font-size: 15px;
449
+ font-family: inherit;
450
+ resize: none;
451
+ outline: none;
452
+ transition: all 0.2s ease;
453
+ min-height: 52px;
454
+ max-height: 150px;
455
+ line-height: 1.5;
456
+ }
457
+
458
+ #message-input::placeholder {
459
+ color: var(--text-muted);
460
+ }
461
+
462
+ #message-input:focus {
463
+ border-color: var(--accent);
464
+ box-shadow: 0 0 0 4px rgba(124, 111, 247, 0.15), var(--shadow-sm);
465
+ }
466
+
467
+ #send-btn {
468
+ display: flex;
469
+ align-items: center;
470
+ justify-content: center;
471
+ gap: 8px;
472
+ background: linear-gradient(135deg, var(--accent), #6a5ff7);
473
+ color: white;
474
+ border: none;
475
+ border-radius: var(--radius-md);
476
+ padding: 16px 24px;
477
+ font-size: 14px;
478
+ font-weight: 600;
479
+ cursor: pointer;
480
+ transition: all 0.2s ease;
481
+ box-shadow: 0 4px 16px rgba(124, 111, 247, 0.4);
482
+ }
483
+
484
+ #send-btn:hover {
485
+ transform: translateY(-2px);
486
+ box-shadow: 0 8px 24px rgba(124, 111, 247, 0.5);
487
+ }
488
+
489
+ #send-btn:active {
490
+ transform: translateY(0);
491
+ }
492
+
493
+ #send-btn:disabled {
494
+ opacity: 0.5;
495
+ cursor: not-allowed;
496
+ transform: none;
497
+ box-shadow: none;
498
+ }
499
+
500
+ #send-btn svg {
501
+ width: 18px;
502
+ height: 18px;
503
+ }
504
+
505
+ /* Modal */
506
+ .modal-overlay {
507
+ position: fixed;
508
+ top: 0;
509
+ left: 0;
510
+ right: 0;
511
+ bottom: 0;
512
+ background: rgba(5, 5, 10, 0.85);
513
+ backdrop-filter: blur(20px);
514
+ display: flex;
515
+ align-items: center;
516
+ justify-content: center;
517
+ z-index: 1000;
518
+ animation: fadeIn 0.3s ease;
519
+ }
520
+
521
+ @keyframes fadeIn {
522
+ from { opacity: 0; }
523
+ to { opacity: 1; }
524
+ }
525
+
526
+ .modal-overlay.hidden {
527
+ display: none;
528
+ }
529
+
530
+ .modal {
531
+ background: var(--bg-elevated);
532
+ border-radius: var(--radius-xl);
533
+ padding: 40px;
534
+ max-width: 420px;
535
+ width: 90%;
536
+ text-align: center;
537
+ border: 1px solid var(--border);
538
+ box-shadow: var(--shadow-md);
539
+ animation: modalIn 0.4s ease;
540
+ }
541
+
542
+ @keyframes modalIn {
543
+ from {
544
+ opacity: 0;
545
+ transform: scale(0.95) translateY(20px);
546
+ }
547
+ to {
548
+ opacity: 1;
549
+ transform: scale(1) translateY(0);
550
+ }
551
+ }
552
+
553
+ .modal-icon {
554
+ width: 72px;
555
+ height: 72px;
556
+ background: linear-gradient(135deg, var(--accent), var(--accent-light));
557
+ border-radius: 50%;
558
+ display: flex;
559
+ align-items: center;
560
+ justify-content: center;
561
+ margin: 0 auto 24px;
562
+ box-shadow: 0 8px 32px rgba(124, 111, 247, 0.3);
563
+ }
564
+
565
+ .modal-icon svg {
566
+ width: 36px;
567
+ height: 36px;
568
+ color: white;
569
+ }
570
+
571
+ .modal h2 {
572
+ font-size: 22px;
573
+ font-weight: 700;
574
+ margin-bottom: 12px;
575
+ background: linear-gradient(135deg, var(--text-primary), var(--accent-light));
576
+ -webkit-background-clip: text;
577
+ -webkit-text-fill-color: transparent;
578
+ background-clip: text;
579
+ }
580
+
581
+ .modal p {
582
+ color: var(--text-secondary);
583
+ margin-bottom: 28px;
584
+ line-height: 1.6;
585
+ font-size: 15px;
586
+ }
587
+
588
+ .modal input {
589
+ width: 100%;
590
+ background: var(--bg-surface);
591
+ border: 1px solid var(--border);
592
+ border-radius: var(--radius-md);
593
+ padding: 16px 20px;
594
+ color: var(--text-primary);
595
+ font-size: 16px;
596
+ font-family: inherit;
597
+ outline: none;
598
+ margin-bottom: 12px;
599
+ text-align: center;
600
+ transition: all 0.2s ease;
601
+ }
602
+
603
+ .modal input:focus {
604
+ border-color: var(--accent);
605
+ box-shadow: 0 0 0 4px rgba(124, 111, 247, 0.15);
606
+ }
607
+
608
+ .modal input::placeholder {
609
+ color: var(--text-muted);
610
+ }
611
+
612
+ .modal .hint {
613
+ font-size: 13px;
614
+ color: var(--text-muted);
615
+ margin-bottom: 28px;
616
+ }
617
+
618
+ .modal button {
619
+ background: linear-gradient(135deg, var(--accent), #6a5ff7);
620
+ color: white;
621
+ border: none;
622
+ border-radius: var(--radius-md);
623
+ padding: 16px 40px;
624
+ font-size: 15px;
625
+ font-weight: 600;
626
+ cursor: pointer;
627
+ transition: all 0.2s ease;
628
+ box-shadow: 0 4px 20px rgba(124, 111, 247, 0.4);
629
+ }
630
+
631
+ .modal button:hover:not(:disabled) {
632
+ transform: translateY(-2px);
633
+ box-shadow: 0 8px 28px rgba(124, 111, 247, 0.5);
634
+ }
635
+
636
+ .modal button:disabled {
637
+ opacity: 0.5;
638
+ cursor: not-allowed;
639
+ }
640
+ </style>
641
+ </head>
642
+ <body>
643
+ <!-- 设置称呼弹窗 -->
644
+ <div class="modal-overlay" id="setup-modal">
645
+ <div class="modal">
646
+ <div class="modal-icon">
647
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
648
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
649
+ </svg>
650
+ </div>
651
+ <h2>欢迎使用 Ailo</h2>
652
+ <p>请告诉我应该如何称呼你?<br>昵称仅保存在本设备浏览器中。</p>
653
+ <input type="text" id="user-name-input" placeholder="请输入你的称呼" maxlength="20">
654
+ <p class="hint">例如:老板、主人、大侠、居士、师父 等</p>
655
+ <button id="setup-btn" disabled>开始聊天</button>
656
+ </div>
657
+ </div>
658
+
659
+ <header>
660
+ <div class="brand">
661
+ <div class="brand-icon">
662
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round">
663
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
664
+ </svg>
665
+ </div>
666
+ <h1>Ailo 智能助手</h1>
667
+ </div>
668
+ <div class="header-right">
669
+ <div class="status">
670
+ <span class="status-dot" id="status-dot"></span>
671
+ <span id="status-text">连接中...</span>
672
+ </div>
673
+ <div class="header-actions">
674
+ <button id="clear-history-btn" class="clear-history-btn" title="清空历史聊天记录">
675
+ 清空聊天
676
+ </button>
677
+ <div class="user-badge" id="user-badge" style="display: none;">
678
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
679
+ <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/>
680
+ <circle cx="12" cy="7" r="4"/>
681
+ </svg>
682
+ <span id="user-name-display"></span>
683
+ </div>
684
+ </div>
685
+ </div>
686
+ </header>
687
+
688
+ <div id="chat-container">
689
+ <div class="empty-state" id="empty-state">
690
+ <div class="empty-state-icon">
691
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
692
+ <path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5z"/>
693
+ </svg>
694
+ </div>
695
+ <p>发送消息开始对话</p>
696
+ <p class="hint">我会尽力帮助你</p>
697
+ </div>
698
+ </div>
699
+
700
+ <div id="input-container">
701
+ <textarea id="message-input" placeholder="输入消息,按 Enter 发送..." rows="1"></textarea>
702
+ <button id="send-btn" title="发送 (Enter)">
703
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
704
+ <line x1="22" y1="2" x2="11" y2="13"/>
705
+ <polygon points="22 2 15 22 11 13 2 9 22 2"/>
706
+ </svg>
707
+ </button>
708
+ </div>
709
+
710
+ <script>
711
+ const chatContainer = document.getElementById('chat-container');
712
+ const emptyState = document.getElementById('empty-state');
713
+ const messageInput = document.getElementById('message-input');
714
+ const sendBtn = document.getElementById('send-btn');
715
+ const statusDot = document.getElementById('status-dot');
716
+ const statusText = document.getElementById('status-text');
717
+
718
+ const setupModal = document.getElementById('setup-modal');
719
+ const userNameInput = document.getElementById('user-name-input');
720
+ const setupBtn = document.getElementById('setup-btn');
721
+ const userBadge = document.getElementById('user-badge');
722
+ const userNameDisplay = document.getElementById('user-name-display');
723
+ const clearHistoryBtn = document.getElementById('clear-history-btn');
724
+
725
+ const STORAGE_KEY = 'ailo_webchat_chat';
726
+ const STORAGE_KEY_NICKNAME = 'ailo_webchat_nickname';
727
+ const MAX_MESSAGES = 200;
728
+
729
+ let ws = null;
730
+ let reconnectTimer = null;
731
+ let userName = null;
732
+
733
+ function loadNickname() {
734
+ try {
735
+ return localStorage.getItem(STORAGE_KEY_NICKNAME) || null;
736
+ } catch { return null; }
737
+ }
738
+
739
+ function saveNickname(name) {
740
+ try {
741
+ localStorage.setItem(STORAGE_KEY_NICKNAME, name);
742
+ } catch (e) { console.warn('saveNickname failed', e); }
743
+ }
744
+
745
+ function initNicknameUI() {
746
+ userName = loadNickname();
747
+ if (userName) {
748
+ userNameDisplay.textContent = userName;
749
+ userBadge.style.display = 'inline-flex';
750
+ setupModal.classList.add('hidden');
751
+ } else {
752
+ setupModal.classList.remove('hidden');
753
+ userNameInput.focus();
754
+ }
755
+ }
756
+
757
+ function loadFromStorage() {
758
+ try {
759
+ const raw = localStorage.getItem(STORAGE_KEY);
760
+ return raw ? JSON.parse(raw) : [];
761
+ } catch { return []; }
762
+ }
763
+
764
+ function saveToStorage(messages) {
765
+ try {
766
+ const trimmed = messages.length > MAX_MESSAGES ? messages.slice(-MAX_MESSAGES) : messages;
767
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(trimmed));
768
+ } catch (e) { console.warn('saveToStorage failed', e); }
769
+ }
770
+
771
+ function clearChatHistory() {
772
+ const hasMessages = loadFromStorage().length > 0;
773
+ if (!hasMessages) return;
774
+
775
+ const confirmed = window.confirm('确认清空所有历史聊天记录吗?此操作不可恢复。');
776
+ if (!confirmed) return;
777
+
778
+ try {
779
+ localStorage.removeItem(STORAGE_KEY);
780
+ } catch (e) {
781
+ console.warn('clearChatHistory failed', e);
782
+ }
783
+
784
+ chatContainer.querySelectorAll('.message').forEach(el => el.remove());
785
+ emptyState.style.display = 'flex';
786
+ updateClearButtonState();
787
+ }
788
+
789
+ function updateClearButtonState() {
790
+ clearHistoryBtn.disabled = loadFromStorage().length === 0;
791
+ }
792
+
793
+ function getInitials(name) {
794
+ if (!name) return '?';
795
+ return name.charAt(0).toUpperCase();
796
+ }
797
+
798
+ function formatTime(timestamp) {
799
+ const date = new Date(timestamp);
800
+ const now = new Date();
801
+ const isToday = date.toDateString() === now.toDateString();
802
+
803
+ if (isToday) {
804
+ return date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
805
+ }
806
+ return date.toLocaleDateString('zh-CN', { month: 'short', day: 'numeric' }) + ' ' +
807
+ date.toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' });
808
+ }
809
+
810
+ userNameInput.addEventListener('input', () => {
811
+ setupBtn.disabled = userNameInput.value.trim().length === 0;
812
+ });
813
+
814
+ userNameInput.addEventListener('keydown', (e) => {
815
+ if (e.key === 'Enter' && userNameInput.value.trim()) {
816
+ setupBtn.click();
817
+ }
818
+ });
819
+
820
+ setupBtn.addEventListener('click', () => {
821
+ const name = userNameInput.value.trim();
822
+ if (!name) return;
823
+
824
+ saveNickname(name);
825
+ userName = name;
826
+ userNameDisplay.textContent = name;
827
+ userBadge.style.display = 'inline-flex';
828
+ setupModal.classList.add('hidden');
829
+ if (ws && ws.readyState === WebSocket.OPEN) {
830
+ ws.send(JSON.stringify({ type: 'register', participantName: userName }));
831
+ }
832
+ });
833
+
834
+ function connect() {
835
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
836
+ ws = new WebSocket(`${protocol}//${window.location.host}/chat/ws`);
837
+
838
+ statusDot.className = 'status-dot connecting';
839
+
840
+ ws.onmessage = (event) => {
841
+ try {
842
+ const data = JSON.parse(event.data);
843
+ if (data.type === 'reply') {
844
+ addMessage('ailo', data.text || '', data.content || []);
845
+ }
846
+ } catch (err) {
847
+ console.error('Failed to parse message:', err);
848
+ }
849
+ };
850
+
851
+ ws.onopen = () => {
852
+ statusDot.className = 'status-dot connected';
853
+ statusText.textContent = '在线';
854
+ if (reconnectTimer) {
855
+ clearTimeout(reconnectTimer);
856
+ reconnectTimer = null;
857
+ }
858
+ if (userName) {
859
+ ws.send(JSON.stringify({ type: 'register', participantName: userName }));
860
+ }
861
+ renderFromStorage();
862
+ };
863
+
864
+ ws.onclose = () => {
865
+ statusDot.className = 'status-dot disconnected';
866
+ statusText.textContent = '离线';
867
+ reconnectTimer = setTimeout(connect, 3000);
868
+ };
869
+
870
+ ws.onerror = () => {
871
+ statusDot.className = 'status-dot disconnected';
872
+ statusText.textContent = '错误';
873
+ };
874
+ }
875
+
876
+ function sendMessage() {
877
+ const text = messageInput.value.trim();
878
+ if (!text || !ws || ws.readyState !== WebSocket.OPEN) return;
879
+
880
+ appendAndSave('user', text, Date.now(), []);
881
+ messageInput.value = '';
882
+ messageInput.style.height = 'auto';
883
+
884
+ ws.send(JSON.stringify({ type: 'chat', text, participantName: userName || '用户' }));
885
+ }
886
+
887
+ function buildBubbleHTML(text, content) {
888
+ // content 是 WebchatContentItem[] 或空数组
889
+ if (!content || content.length === 0) {
890
+ return `<div class="message-bubble">${escapeHtml(text || '')}</div>`;
891
+ }
892
+ let inner = '';
893
+ for (const item of content) {
894
+ if (item.kind === 'text') {
895
+ inner += `<div>${escapeHtml(item.text || '')}</div>`;
896
+ } else if (item.kind === 'image') {
897
+ inner += `<img class="chat-img" src="${escapeHtml(item.url || '')}" alt="${escapeHtml(item.name || '图片')}" onclick="window.open(this.src,'_blank')">`;
898
+ } else if (item.kind === 'file') {
899
+ inner += `<a class="chat-file" href="${escapeHtml(item.url || '')}" target="_blank" rel="noopener" download="${escapeHtml(item.name || 'file')}"><span class="chat-file-icon">📎</span><span class="chat-file-name">${escapeHtml(item.name || '文件')}</span></a>`;
900
+ }
901
+ }
902
+ return `<div class="message-bubble">${inner}</div>`;
903
+ }
904
+
905
+ function appendAndSave(sender, text, timestamp, content) {
906
+ emptyState.style.display = 'none';
907
+ const msgs = loadFromStorage();
908
+ msgs.push({ sender, text, timestamp, content: content || [] });
909
+ saveToStorage(msgs);
910
+
911
+ const div = document.createElement('div');
912
+ div.className = `message ${sender}`;
913
+
914
+ const avatarInitials = sender === 'user' ? getInitials(userName) : 'A';
915
+ const senderLabel = sender === 'user' ? (userName || '你') : 'Ailo';
916
+
917
+ div.innerHTML = `
918
+ <div class="message-avatar">${avatarInitials}</div>
919
+ <div class="message-content">
920
+ <div class="sender">${senderLabel}</div>
921
+ ${buildBubbleHTML(text, content || [])}
922
+ <div class="message-time">${formatTime(timestamp)}</div>
923
+ </div>
924
+ `;
925
+
926
+ chatContainer.appendChild(div);
927
+ chatContainer.scrollTop = chatContainer.scrollHeight;
928
+ updateClearButtonState();
929
+ }
930
+
931
+ function addMessage(sender, text, content) {
932
+ appendAndSave(sender, text, Date.now(), content || []);
933
+ }
934
+
935
+ function renderFromStorage() {
936
+ const msgs = loadFromStorage();
937
+ if (!msgs.length) {
938
+ updateClearButtonState();
939
+ return;
940
+ }
941
+ emptyState.style.display = 'none';
942
+ chatContainer.querySelectorAll('.message').forEach(el => el.remove());
943
+
944
+ for (const m of msgs) {
945
+ const div = document.createElement('div');
946
+ div.className = `message ${m.sender}`;
947
+
948
+ const avatarInitials = m.sender === 'user' ? getInitials(userName) : 'A';
949
+ const senderLabel = m.sender === 'user' ? (userName || '你') : 'Ailo';
950
+ const timestamp = m.timestamp || Date.now();
951
+ const content = m.content || [];
952
+
953
+ div.innerHTML = `
954
+ <div class="message-avatar">${avatarInitials}</div>
955
+ <div class="message-content">
956
+ <div class="sender">${senderLabel}</div>
957
+ ${buildBubbleHTML(m.text ?? '', content)}
958
+ <div class="message-time">${formatTime(timestamp)}</div>
959
+ </div>
960
+ `;
961
+ chatContainer.appendChild(div);
962
+ }
963
+ chatContainer.scrollTop = chatContainer.scrollHeight;
964
+ updateClearButtonState();
965
+ }
966
+
967
+ function escapeHtml(text) {
968
+ const div = document.createElement('div');
969
+ div.textContent = text;
970
+ return div.innerHTML;
971
+ }
972
+
973
+ sendBtn.addEventListener('click', sendMessage);
974
+ clearHistoryBtn.addEventListener('click', clearChatHistory);
975
+
976
+ messageInput.addEventListener('keydown', (e) => {
977
+ if (e.key === 'Enter' && !e.shiftKey) {
978
+ e.preventDefault();
979
+ sendMessage();
980
+ }
981
+ });
982
+
983
+ messageInput.addEventListener('input', () => {
984
+ messageInput.style.height = 'auto';
985
+ messageInput.style.height = Math.min(messageInput.scrollHeight, 150) + 'px';
986
+ });
987
+
988
+ initNicknameUI();
989
+ renderFromStorage();
990
+ updateClearButtonState();
991
+ connect();
992
+ </script>
993
+ </body>
994
+ </html>