aicq-chat-plugin 3.9.0 → 3.9.1
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/README.md +80 -80
- package/SKILL.md +78 -78
- package/cli.cjs +356 -356
- package/index.js +417 -385
- package/lib/chat.js +854 -971
- package/lib/crypto.js +168 -168
- package/lib/database.js +455 -455
- package/lib/file-transfer.js +266 -266
- package/lib/handshake.js +147 -147
- package/lib/identity.js +165 -165
- package/lib/package.json +3 -3
- package/lib/server-client.js +380 -337
- package/openclaw.plugin.json +170 -168
- package/package.json +87 -87
- package/postinstall.cjs +27 -27
- package/public/favicon.ico +0 -0
- package/public/icon-16.png +0 -0
- package/public/icon-32.png +0 -0
- package/public/index.html +1468 -1468
- package/public/logo-512.png +0 -0
- package/setup-entry.js +14 -14
- package/src/channel.js +616 -637
- package/src/ui-routes.js +647 -594
package/lib/chat.js
CHANGED
|
@@ -1,971 +1,854 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* AICQ Chat Manager — Send/receive messages, group chat, file
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const
|
|
11
|
-
const
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
this.
|
|
21
|
-
this.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
this.
|
|
25
|
-
|
|
26
|
-
//
|
|
27
|
-
this.
|
|
28
|
-
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
this.server.onMessage('
|
|
39
|
-
this.server.onMessage('
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
this.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
//
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
targetId,
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
return
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
//
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
if (
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
//
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
*
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
const
|
|
778
|
-
const
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
if (
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
/**
|
|
827
|
-
*
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
} else {
|
|
856
|
-
agentMessage = [
|
|
857
|
-
`[用户上传了文件]`,
|
|
858
|
-
`文件名: ${fileName}`,
|
|
859
|
-
`文件路径: ${filePath}`,
|
|
860
|
-
`文件大小: ${this._formatFileSize(fileSize)}`,
|
|
861
|
-
`文件类型: ${mimeType}`,
|
|
862
|
-
caption ? `说明: ${caption}` : '',
|
|
863
|
-
`请读取并处理这个文件。`,
|
|
864
|
-
].filter(Boolean).join('\n');
|
|
865
|
-
}
|
|
866
|
-
|
|
867
|
-
// Save this as a text message in chat history so the agent sees it
|
|
868
|
-
const msg = this.db.saveMessage({
|
|
869
|
-
agent_id: agentId,
|
|
870
|
-
target_id: fromId,
|
|
871
|
-
from_id: fromId,
|
|
872
|
-
to_id: agentId,
|
|
873
|
-
type: 'text',
|
|
874
|
-
content: agentMessage,
|
|
875
|
-
file_url: filePath,
|
|
876
|
-
file_name: fileName,
|
|
877
|
-
is_group: isGroup ? 1 : 0,
|
|
878
|
-
status: 'delivered',
|
|
879
|
-
});
|
|
880
|
-
|
|
881
|
-
// Trigger the onNewMessage callback so the channel.js inbound handler
|
|
882
|
-
// picks it up and dispatches it to the AI agent
|
|
883
|
-
if (this._onNewMessage) {
|
|
884
|
-
this._onNewMessage(msg);
|
|
885
|
-
}
|
|
886
|
-
|
|
887
|
-
console.log(`[Chat] Agent notification sent: ${msgType} ${fileName} at ${filePath}`);
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
/**
|
|
891
|
-
* Format file size in human-readable format.
|
|
892
|
-
*/
|
|
893
|
-
_formatFileSize(bytes) {
|
|
894
|
-
if (bytes === 0) return '0 B';
|
|
895
|
-
const units = ['B', 'KB', 'MB', 'GB'];
|
|
896
|
-
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
|
897
|
-
return (bytes / Math.pow(1024, i)).toFixed(1) + ' ' + units[i];
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
/**
|
|
901
|
-
* List all files in the userfiles directory.
|
|
902
|
-
* @returns {Array} Array of file info objects
|
|
903
|
-
*/
|
|
904
|
-
listUserfiles() {
|
|
905
|
-
if (!fs.existsSync(this.userfilesDir)) return [];
|
|
906
|
-
return fs.readdirSync(this.userfilesDir)
|
|
907
|
-
.filter(name => !name.startsWith('.'))
|
|
908
|
-
.map(name => {
|
|
909
|
-
const fullPath = path.join(this.userfilesDir, name);
|
|
910
|
-
try {
|
|
911
|
-
const stat = fs.statSync(fullPath);
|
|
912
|
-
return {
|
|
913
|
-
name,
|
|
914
|
-
path: fullPath,
|
|
915
|
-
size: stat.size,
|
|
916
|
-
mimeType: this._getMimeType(name),
|
|
917
|
-
isImage: this._isImageExt(path.extname(name)),
|
|
918
|
-
modifiedAt: stat.mtime.toISOString(),
|
|
919
|
-
};
|
|
920
|
-
} catch (e) {
|
|
921
|
-
return null;
|
|
922
|
-
}
|
|
923
|
-
})
|
|
924
|
-
.filter(Boolean);
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
/**
|
|
928
|
-
* Get the userfiles directory path.
|
|
929
|
-
* @returns {string} Absolute path to userfiles directory
|
|
930
|
-
*/
|
|
931
|
-
getUserfilesDir() {
|
|
932
|
-
return this.userfilesDir;
|
|
933
|
-
}
|
|
934
|
-
|
|
935
|
-
// ─── Helpers ────────────────────────────────────────────────────
|
|
936
|
-
|
|
937
|
-
_isImageExt(ext) {
|
|
938
|
-
return /\.(jpg|jpeg|png|gif|webp|svg|bmp|ico|tiff|tif|avif)$/i.test(ext);
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
_getMimeType(fileName) {
|
|
942
|
-
const ext = path.extname(fileName).toLowerCase();
|
|
943
|
-
const mimeTypes = {
|
|
944
|
-
'.png': 'image/png', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg',
|
|
945
|
-
'.gif': 'image/gif', '.webp': 'image/webp', '.svg': 'image/svg+xml',
|
|
946
|
-
'.bmp': 'image/bmp', '.ico': 'image/x-icon', '.tiff': 'image/tiff',
|
|
947
|
-
'.tif': 'image/tiff', '.avif': 'image/avif',
|
|
948
|
-
'.pdf': 'application/pdf', '.txt': 'text/plain', '.json': 'application/json',
|
|
949
|
-
'.zip': 'application/zip', '.mp3': 'audio/mpeg', '.wav': 'audio/wav',
|
|
950
|
-
'.mp4': 'video/mp4', '.webm': 'video/webm', '.ogg': 'audio/ogg',
|
|
951
|
-
'.doc': 'application/msword', '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
952
|
-
'.xls': 'application/vnd.ms-excel', '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
953
|
-
'.ppt': 'application/vnd.ms-powerpoint', '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
954
|
-
};
|
|
955
|
-
return mimeTypes[ext] || 'application/octet-stream';
|
|
956
|
-
}
|
|
957
|
-
|
|
958
|
-
_extFromMime(mimeType) {
|
|
959
|
-
if (!mimeType) return '';
|
|
960
|
-
const mimeToExt = {
|
|
961
|
-
'image/png': '.png', 'image/jpeg': '.jpg', 'image/gif': '.gif',
|
|
962
|
-
'image/webp': '.webp', 'image/svg+xml': '.svg', 'image/bmp': '.bmp',
|
|
963
|
-
'application/pdf': '.pdf', 'text/plain': '.txt',
|
|
964
|
-
'application/zip': '.zip', 'audio/mpeg': '.mp3',
|
|
965
|
-
'video/mp4': '.mp4', 'audio/wav': '.wav',
|
|
966
|
-
};
|
|
967
|
-
return mimeToExt[mimeType] || '';
|
|
968
|
-
}
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
module.exports = ChatManager;
|
|
1
|
+
/**
|
|
2
|
+
* AICQ Chat Manager — Send/receive messages, group chat, file handling
|
|
3
|
+
*
|
|
4
|
+
* Enhanced: File/image messages received from users are saved to the
|
|
5
|
+
* `userfiles` directory. After saving, a synthetic message is injected
|
|
6
|
+
* into the AI dispatch pipeline that tells the agent about the local
|
|
7
|
+
* file path so it can process the file (read, analyze, etc.).
|
|
8
|
+
*/
|
|
9
|
+
const { encryptMessage, decryptMessage } = require('./crypto');
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const crypto = require('crypto');
|
|
13
|
+
|
|
14
|
+
class ChatManager {
|
|
15
|
+
constructor(identityManager, serverClient, db, uploadsDir, userfilesDir) {
|
|
16
|
+
this.identity = identityManager;
|
|
17
|
+
this.server = serverClient;
|
|
18
|
+
this.db = db;
|
|
19
|
+
this.uploadsDir = uploadsDir;
|
|
20
|
+
this.userfilesDir = userfilesDir || path.join(path.dirname(uploadsDir), 'userfiles');
|
|
21
|
+
this._onNewMessage = null;
|
|
22
|
+
|
|
23
|
+
// Ensure userfiles directory exists
|
|
24
|
+
fs.mkdirSync(this.userfilesDir, { recursive: true });
|
|
25
|
+
|
|
26
|
+
// Incoming file chunk assembly state: fileId -> { meta, chunks }
|
|
27
|
+
this._incomingFiles = new Map();
|
|
28
|
+
|
|
29
|
+
// Listen for incoming messages via WS
|
|
30
|
+
this.server.onMessage('relay', (data) => this._handleIncoming(data));
|
|
31
|
+
this.server.onMessage('message', (data) => this._handleIncoming(data));
|
|
32
|
+
this.server.onMessage('group_message', (data) => this._handleGroupIncoming(data));
|
|
33
|
+
this.server.onMessage('handshake_initiate', (data) => this._handleHandshakeRequest(data));
|
|
34
|
+
this.server.onMessage('presence', (data) => this._handlePresence(data));
|
|
35
|
+
this.server.onMessage('file_chunk', (data) => this._handleFileChunk(data));
|
|
36
|
+
this.server.onMessage('file', (data) => this._handleFileMessage(data));
|
|
37
|
+
this.server.onMessage('image', (data) => this._handleFileMessage(data));
|
|
38
|
+
this.server.onMessage('stream_chunk', (data) => this._handleStreamChunk(data));
|
|
39
|
+
this.server.onMessage('stream_end', (data) => this._handleStreamEnd(data));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
setOnNewMessage(callback) {
|
|
43
|
+
this._onNewMessage = callback;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ─── Send Messages ────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
async sendMessage(agentId, targetId, content, { type = 'text', isGroup = false, mentions = [], file_url = null, file_name = null, local_path = null } = {}) {
|
|
49
|
+
const identity = this.identity.loadAgent(agentId);
|
|
50
|
+
|
|
51
|
+
if (isGroup) {
|
|
52
|
+
// Group message via WebSocket
|
|
53
|
+
const sent = this.server.sendWS({
|
|
54
|
+
type: 'group_message',
|
|
55
|
+
groupId: targetId,
|
|
56
|
+
content,
|
|
57
|
+
msgType: type,
|
|
58
|
+
mentions,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
// Save locally
|
|
62
|
+
const msg = this.db.saveMessage({
|
|
63
|
+
agent_id: agentId,
|
|
64
|
+
target_id: targetId,
|
|
65
|
+
from_id: agentId,
|
|
66
|
+
to_id: targetId,
|
|
67
|
+
type,
|
|
68
|
+
content,
|
|
69
|
+
file_url,
|
|
70
|
+
file_name,
|
|
71
|
+
local_path,
|
|
72
|
+
is_group: 1,
|
|
73
|
+
mentions,
|
|
74
|
+
status: sent ? 'sent' : 'pending',
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
if (this._onNewMessage) this._onNewMessage(msg);
|
|
78
|
+
return msg;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Direct message
|
|
82
|
+
// Try to encrypt if we have a session key
|
|
83
|
+
const session = this.db.loadSession(agentId, targetId);
|
|
84
|
+
let payload = content;
|
|
85
|
+
if (session && session.session_key) {
|
|
86
|
+
try {
|
|
87
|
+
payload = encryptMessage(content, session.session_key);
|
|
88
|
+
} catch (e) {
|
|
89
|
+
console.error('[Chat] Encryption failed, sending plaintext:', e.message);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Send via WebSocket relay
|
|
94
|
+
const sent = this.server.sendWS({
|
|
95
|
+
type: 'relay',
|
|
96
|
+
targetId: targetId,
|
|
97
|
+
payload,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// Also try REST fallback
|
|
101
|
+
if (!sent) {
|
|
102
|
+
try {
|
|
103
|
+
await this.server._request('POST', '/messages/send', {
|
|
104
|
+
targetId,
|
|
105
|
+
payload,
|
|
106
|
+
});
|
|
107
|
+
} catch (e) {
|
|
108
|
+
// Queue offline
|
|
109
|
+
this.db.enqueueOffline({
|
|
110
|
+
agent_id: agentId,
|
|
111
|
+
target_id: targetId,
|
|
112
|
+
data: JSON.stringify({ type: 'relay', targetId, payload }),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Save locally
|
|
118
|
+
const msg = this.db.saveMessage({
|
|
119
|
+
agent_id: agentId,
|
|
120
|
+
target_id: targetId,
|
|
121
|
+
from_id: agentId,
|
|
122
|
+
to_id: targetId,
|
|
123
|
+
type,
|
|
124
|
+
content,
|
|
125
|
+
file_url,
|
|
126
|
+
file_name,
|
|
127
|
+
local_path,
|
|
128
|
+
is_group: 0,
|
|
129
|
+
mentions,
|
|
130
|
+
status: sent ? 'sent' : 'pending',
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Update session message count
|
|
134
|
+
if (session) {
|
|
135
|
+
this.db.incrementSessionMessageCount(agentId, targetId);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (this._onNewMessage) this._onNewMessage(msg);
|
|
139
|
+
return msg;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ─── Receive Messages ─────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
_handleIncoming(data) {
|
|
145
|
+
const agentId = this.server.currentAgentId;
|
|
146
|
+
if (!agentId) return;
|
|
147
|
+
|
|
148
|
+
const fromId = data.fromId || data.from;
|
|
149
|
+
let content = data.payload || data.data || '';
|
|
150
|
+
const msgType = data.msgType || data.type || 'text';
|
|
151
|
+
|
|
152
|
+
// Try to decrypt if we have a session key
|
|
153
|
+
const session = this.db.loadSession(agentId, fromId);
|
|
154
|
+
if (session && session.session_key && typeof content === 'string') {
|
|
155
|
+
try {
|
|
156
|
+
content = decryptMessage(content, session.session_key);
|
|
157
|
+
} catch (e) {
|
|
158
|
+
// Might be plaintext, keep as is
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Detect if this is a file or image message
|
|
163
|
+
const isFileMessage = this._isFileMessage(msgType, content, data);
|
|
164
|
+
let localFilePath = null;
|
|
165
|
+
let originalFileName = null;
|
|
166
|
+
|
|
167
|
+
if (isFileMessage) {
|
|
168
|
+
const fileResult = this._saveIncomingFileToUserfiles(agentId, fromId, content, data);
|
|
169
|
+
if (fileResult) {
|
|
170
|
+
localFilePath = fileResult.localPath;
|
|
171
|
+
originalFileName = fileResult.originalName;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Save the original message
|
|
176
|
+
const msg = this.db.saveMessage({
|
|
177
|
+
agent_id: agentId,
|
|
178
|
+
target_id: fromId,
|
|
179
|
+
from_id: fromId,
|
|
180
|
+
to_id: agentId,
|
|
181
|
+
type: isFileMessage ? (this._isImageMessage(msgType, content, data) ? 'image' : 'file') : 'text',
|
|
182
|
+
content: typeof content === 'string' ? content : JSON.stringify(content),
|
|
183
|
+
file_url: data.file_url || data.fileUrl || null,
|
|
184
|
+
file_name: originalFileName || data.file_name || data.fileName || null,
|
|
185
|
+
local_path: localFilePath,
|
|
186
|
+
is_group: 0,
|
|
187
|
+
status: 'delivered',
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
if (this._onNewMessage) this._onNewMessage(msg);
|
|
191
|
+
|
|
192
|
+
// If this was a file/image message, also inject a synthetic message
|
|
193
|
+
// telling the AI agent about the local file path
|
|
194
|
+
if (isFileMessage && localFilePath && this._onNewMessage) {
|
|
195
|
+
const isImage = this._isImageMessage(msgType, content, data);
|
|
196
|
+
const fileType = isImage ? '图片' : '文件';
|
|
197
|
+
const syntheticMsg = {
|
|
198
|
+
agent_id: agentId,
|
|
199
|
+
target_id: fromId,
|
|
200
|
+
from_id: fromId,
|
|
201
|
+
to_id: agentId,
|
|
202
|
+
type: 'text',
|
|
203
|
+
content: `[用户发送了${fileType}] ${originalFileName || '未知文件名'}\n本地路径: ${localFilePath}\n请处理该${fileType}。`,
|
|
204
|
+
is_group: 0,
|
|
205
|
+
status: 'delivered',
|
|
206
|
+
_synthetic: true, // Mark as synthetic so AI dispatch can handle it
|
|
207
|
+
_original_msg_id: msg.message_id || msg.id,
|
|
208
|
+
};
|
|
209
|
+
this._onNewMessage(syntheticMsg);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
_handleGroupIncoming(data) {
|
|
214
|
+
const agentId = this.server.currentAgentId;
|
|
215
|
+
if (!agentId) return;
|
|
216
|
+
|
|
217
|
+
const fromId = data.fromId;
|
|
218
|
+
const groupId = data.groupId;
|
|
219
|
+
|
|
220
|
+
// Check silent mode
|
|
221
|
+
const silent = this.db.getGroupSilentMode(agentId, groupId);
|
|
222
|
+
const mentions = data.mentions || [];
|
|
223
|
+
const isMentioned = mentions.includes(agentId) || mentions.includes('all');
|
|
224
|
+
|
|
225
|
+
const content = data.content || '';
|
|
226
|
+
const msgType = data.msgType || 'text';
|
|
227
|
+
|
|
228
|
+
// Detect file/image in group message
|
|
229
|
+
const isFileMessage = this._isFileMessage(msgType, content, data);
|
|
230
|
+
let localFilePath = null;
|
|
231
|
+
let originalFileName = null;
|
|
232
|
+
|
|
233
|
+
if (isFileMessage) {
|
|
234
|
+
const fileResult = this._saveIncomingFileToUserfiles(agentId, fromId, content, data);
|
|
235
|
+
if (fileResult) {
|
|
236
|
+
localFilePath = fileResult.localPath;
|
|
237
|
+
originalFileName = fileResult.originalName;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const msg = this.db.saveMessage({
|
|
242
|
+
agent_id: agentId,
|
|
243
|
+
target_id: groupId,
|
|
244
|
+
from_id: fromId,
|
|
245
|
+
to_id: groupId,
|
|
246
|
+
type: isFileMessage ? (this._isImageMessage(msgType, content, data) ? 'image' : 'file') : (data.msgType || 'text'),
|
|
247
|
+
content,
|
|
248
|
+
file_url: data.file_url || data.fileUrl || null,
|
|
249
|
+
file_name: originalFileName || data.file_name || data.fileName || null,
|
|
250
|
+
local_path: localFilePath,
|
|
251
|
+
is_group: 1,
|
|
252
|
+
mentions,
|
|
253
|
+
status: (silent && !isMentioned) ? 'silent' : 'delivered',
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
if (this._onNewMessage) this._onNewMessage(msg);
|
|
257
|
+
|
|
258
|
+
// Inject synthetic message for group file messages
|
|
259
|
+
if (isFileMessage && localFilePath && this._onNewMessage && (isMentioned || !silent)) {
|
|
260
|
+
const isImage = this._isImageMessage(msgType, content, data);
|
|
261
|
+
const fileType = isImage ? '图片' : '文件';
|
|
262
|
+
const syntheticMsg = {
|
|
263
|
+
agent_id: agentId,
|
|
264
|
+
target_id: groupId,
|
|
265
|
+
from_id: fromId,
|
|
266
|
+
to_id: groupId,
|
|
267
|
+
type: 'text',
|
|
268
|
+
content: `[群组中用户发送了${fileType}] ${originalFileName || '未知文件名'}\n本地路径: ${localFilePath}\n请处理该${fileType}。`,
|
|
269
|
+
is_group: 1,
|
|
270
|
+
status: 'delivered',
|
|
271
|
+
_synthetic: true,
|
|
272
|
+
_original_msg_id: msg.message_id || msg.id,
|
|
273
|
+
};
|
|
274
|
+
this._onNewMessage(syntheticMsg);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
_handleHandshakeRequest(data) {
|
|
279
|
+
const agentId = this.server.currentAgentId;
|
|
280
|
+
if (!agentId) return;
|
|
281
|
+
|
|
282
|
+
this.db.savePendingRequest({
|
|
283
|
+
agent_id: agentId,
|
|
284
|
+
session_id: data.sessionId || crypto.randomUUID(),
|
|
285
|
+
requester_id: data.requesterId || data.from,
|
|
286
|
+
requester_public_key: data.requesterPublicKey || data.exchangePublicKey || '',
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
_handlePresence(data) {
|
|
291
|
+
const agentId = this.server.currentAgentId;
|
|
292
|
+
if (!agentId) return;
|
|
293
|
+
|
|
294
|
+
const friendId = data.nodeId;
|
|
295
|
+
const isOnline = data.online === true || data.status === 'online';
|
|
296
|
+
this.db.updateFriendOnline(agentId, friendId, isOnline);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
_handleFileMessage(data) {
|
|
300
|
+
// Handle explicit file/image type WS messages
|
|
301
|
+
const agentId = this.server.currentAgentId;
|
|
302
|
+
if (!agentId) return;
|
|
303
|
+
|
|
304
|
+
const fromId = data.fromId || data.from;
|
|
305
|
+
const content = data.content || data.data || '';
|
|
306
|
+
const isImage = data.type === 'image' || this._isImageMessage(data.type, content, data);
|
|
307
|
+
|
|
308
|
+
let localFilePath = null;
|
|
309
|
+
let originalFileName = null;
|
|
310
|
+
|
|
311
|
+
// If the file data is inline (base64), save it
|
|
312
|
+
if (data.file_data || data.data && this._isBase64Data(data.data)) {
|
|
313
|
+
const fileResult = this._saveBase64FileToUserfiles(agentId, fromId, data);
|
|
314
|
+
if (fileResult) {
|
|
315
|
+
localFilePath = fileResult.localPath;
|
|
316
|
+
originalFileName = fileResult.originalName;
|
|
317
|
+
}
|
|
318
|
+
} else if (data.file_url || data.fileUrl) {
|
|
319
|
+
// Download file from URL and save locally
|
|
320
|
+
const fileResult = this._saveUrlFileToUserfiles(agentId, fromId, data);
|
|
321
|
+
if (fileResult) {
|
|
322
|
+
localFilePath = fileResult.localPath;
|
|
323
|
+
originalFileName = fileResult.originalName;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Save message
|
|
328
|
+
const msg = this.db.saveMessage({
|
|
329
|
+
agent_id: agentId,
|
|
330
|
+
target_id: fromId,
|
|
331
|
+
from_id: fromId,
|
|
332
|
+
to_id: agentId,
|
|
333
|
+
type: isImage ? 'image' : 'file',
|
|
334
|
+
content,
|
|
335
|
+
file_url: data.file_url || data.fileUrl || null,
|
|
336
|
+
file_name: originalFileName || data.file_name || data.fileName || null,
|
|
337
|
+
local_path: localFilePath,
|
|
338
|
+
is_group: 0,
|
|
339
|
+
status: 'delivered',
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
if (this._onNewMessage) this._onNewMessage(msg);
|
|
343
|
+
|
|
344
|
+
// Inject synthetic message
|
|
345
|
+
if (localFilePath && this._onNewMessage) {
|
|
346
|
+
const fileType = isImage ? '图片' : '文件';
|
|
347
|
+
const syntheticMsg = {
|
|
348
|
+
agent_id: agentId,
|
|
349
|
+
target_id: fromId,
|
|
350
|
+
from_id: fromId,
|
|
351
|
+
to_id: agentId,
|
|
352
|
+
type: 'text',
|
|
353
|
+
content: `[用户发送了${fileType}] ${originalFileName || '未知文件名'}\n本地路径: ${localFilePath}\n请处理该${fileType}。`,
|
|
354
|
+
is_group: 0,
|
|
355
|
+
status: 'delivered',
|
|
356
|
+
_synthetic: true,
|
|
357
|
+
_original_msg_id: msg.message_id || msg.id,
|
|
358
|
+
};
|
|
359
|
+
this._onNewMessage(syntheticMsg);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
_handleFileChunk(data) {
|
|
364
|
+
// File chunk handling — assemble in userfiles dir
|
|
365
|
+
const agentId = this.server.currentAgentId;
|
|
366
|
+
if (!agentId) return;
|
|
367
|
+
|
|
368
|
+
const chunkData = data.data || data;
|
|
369
|
+
const fileId = chunkData.fileId || data.fileId;
|
|
370
|
+
|
|
371
|
+
if (!fileId) {
|
|
372
|
+
console.log('[Chat] File chunk without fileId from', data.from);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Initialize incoming transfer if needed
|
|
377
|
+
if (!this._incomingFiles.has(fileId)) {
|
|
378
|
+
this._incomingFiles.set(fileId, {
|
|
379
|
+
chunks: new Map(),
|
|
380
|
+
meta: null,
|
|
381
|
+
fromId: data.fromId || data.from,
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const transfer = this._incomingFiles.get(fileId);
|
|
386
|
+
|
|
387
|
+
// If this is a file-info message
|
|
388
|
+
if (chunkData.type === 'file-info') {
|
|
389
|
+
transfer.meta = chunkData;
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// Store the chunk
|
|
394
|
+
transfer.chunks.set(chunkData.index, chunkData);
|
|
395
|
+
|
|
396
|
+
// Check if all chunks received
|
|
397
|
+
if (transfer.meta && transfer.chunks.size >= transfer.meta.totalChunks) {
|
|
398
|
+
this._assembleAndNotify(agentId, fileId, transfer);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Assemble received file chunks into a complete file in userfiles,
|
|
404
|
+
* then notify the AI agent about the local file path.
|
|
405
|
+
*/
|
|
406
|
+
_assembleAndNotify(agentId, fileId, transfer) {
|
|
407
|
+
const { meta, chunks, fromId } = transfer;
|
|
408
|
+
|
|
409
|
+
try {
|
|
410
|
+
const sortedChunks = Array.from(chunks.entries())
|
|
411
|
+
.sort((a, b) => a[0] - b[0]);
|
|
412
|
+
|
|
413
|
+
const buffers = [];
|
|
414
|
+
for (const [index, chunk] of sortedChunks) {
|
|
415
|
+
if (chunk.encrypted) {
|
|
416
|
+
// For now, try to use raw data
|
|
417
|
+
buffers.push(Buffer.from(chunk.data, 'base64'));
|
|
418
|
+
} else {
|
|
419
|
+
buffers.push(Buffer.from(chunk.data, 'base64'));
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const fileBuffer = Buffer.concat(buffers);
|
|
424
|
+
const originalName = meta.fileName || `file_${fileId}`;
|
|
425
|
+
const ext = path.extname(originalName) || '.bin';
|
|
426
|
+
|
|
427
|
+
// Save to userfiles with timestamp prefix for uniqueness
|
|
428
|
+
const timestamp = Date.now();
|
|
429
|
+
const safeName = `${timestamp}_${fileId.substring(0, 8)}${ext}`;
|
|
430
|
+
const localPath = path.join(this.userfilesDir, safeName);
|
|
431
|
+
fs.writeFileSync(localPath, fileBuffer);
|
|
432
|
+
|
|
433
|
+
const isImage = /\.(jpg|jpeg|png|gif|webp|svg|bmp)$/i.test(ext);
|
|
434
|
+
|
|
435
|
+
// Save to chat history
|
|
436
|
+
const msg = this.db.saveMessage({
|
|
437
|
+
agent_id: agentId,
|
|
438
|
+
target_id: fromId,
|
|
439
|
+
from_id: fromId,
|
|
440
|
+
to_id: agentId,
|
|
441
|
+
type: isImage ? 'image' : 'file',
|
|
442
|
+
content: JSON.stringify({
|
|
443
|
+
fileId,
|
|
444
|
+
fileName: originalName,
|
|
445
|
+
fileSize: meta.fileSize,
|
|
446
|
+
localPath,
|
|
447
|
+
}),
|
|
448
|
+
file_name: originalName,
|
|
449
|
+
local_path: localPath,
|
|
450
|
+
is_group: 0,
|
|
451
|
+
status: 'delivered',
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
console.log(`[Chat] File assembled: ${originalName} -> ${localPath}`);
|
|
455
|
+
|
|
456
|
+
if (this._onNewMessage) {
|
|
457
|
+
this._onNewMessage(msg);
|
|
458
|
+
|
|
459
|
+
// Inject synthetic message
|
|
460
|
+
const fileType = isImage ? '图片' : '文件';
|
|
461
|
+
const syntheticMsg = {
|
|
462
|
+
agent_id: agentId,
|
|
463
|
+
target_id: fromId,
|
|
464
|
+
from_id: fromId,
|
|
465
|
+
to_id: agentId,
|
|
466
|
+
type: 'text',
|
|
467
|
+
content: `[用户发送了${fileType}] ${originalName}\n本地路径: ${localPath}\n文件大小: ${meta.fileSize} 字节\n请处理该${fileType}。`,
|
|
468
|
+
is_group: 0,
|
|
469
|
+
status: 'delivered',
|
|
470
|
+
_synthetic: true,
|
|
471
|
+
_original_msg_id: msg.message_id || msg.id,
|
|
472
|
+
};
|
|
473
|
+
this._onNewMessage(syntheticMsg);
|
|
474
|
+
}
|
|
475
|
+
} catch (e) {
|
|
476
|
+
console.error(`[Chat] File assembly failed for ${fileId}:`, e.message);
|
|
477
|
+
} finally {
|
|
478
|
+
this._incomingFiles.delete(fileId);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
_handleStreamChunk(data) {
|
|
483
|
+
// Incoming streaming chunk from another agent
|
|
484
|
+
const agentId = this.server.currentAgentId;
|
|
485
|
+
if (!agentId) return;
|
|
486
|
+
|
|
487
|
+
const fromId = data.from;
|
|
488
|
+
const chunkType = data.chunkType || 'text';
|
|
489
|
+
const chunkData = data.data;
|
|
490
|
+
|
|
491
|
+
// Notify callback so OpenClaw agent can process streaming input
|
|
492
|
+
if (this._onNewMessage) {
|
|
493
|
+
this._onNewMessage({
|
|
494
|
+
type: 'stream_chunk',
|
|
495
|
+
from_id: fromId,
|
|
496
|
+
chunk_type: chunkType,
|
|
497
|
+
data: chunkData,
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
console.log('[Chat] Stream chunk from', fromId, 'type:', chunkType);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
_handleStreamEnd(data) {
|
|
504
|
+
// Incoming stream end signal from another agent
|
|
505
|
+
const agentId = this.server.currentAgentId;
|
|
506
|
+
if (!agentId) return;
|
|
507
|
+
|
|
508
|
+
const fromId = data.from;
|
|
509
|
+
const messageId = data.messageId || '';
|
|
510
|
+
|
|
511
|
+
// Notify callback so OpenClaw agent knows stream is complete
|
|
512
|
+
if (this._onNewMessage) {
|
|
513
|
+
this._onNewMessage({
|
|
514
|
+
type: 'stream_end',
|
|
515
|
+
from_id: fromId,
|
|
516
|
+
message_id: messageId,
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
console.log('[Chat] Stream end from', fromId, 'messageId:', messageId);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// ─── Chat History ─────────────────────────────────────────────────
|
|
523
|
+
|
|
524
|
+
getHistory(agentId, targetId, { limit = 50, before = null } = {}) {
|
|
525
|
+
return this.db.getChatHistory(agentId, targetId, { limit, before });
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
deleteMessage(agentId, messageId) {
|
|
529
|
+
this.db.deleteMessage(agentId, messageId);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
// ─── File Upload ──────────────────────────────────────────────────
|
|
533
|
+
|
|
534
|
+
async handleFileUpload(agentId, targetId, file, isGroup = false) {
|
|
535
|
+
const fileId = crypto.randomUUID();
|
|
536
|
+
const ext = path.extname(file.originalname || '.bin');
|
|
537
|
+
const fileName = `${fileId}${ext}`;
|
|
538
|
+
const filePath = path.join(this.uploadsDir, fileName);
|
|
539
|
+
fs.writeFileSync(filePath, file.buffer);
|
|
540
|
+
|
|
541
|
+
const isImage = /\.(jpg|jpeg|png|gif|webp|svg|bmp)$/i.test(ext);
|
|
542
|
+
|
|
543
|
+
// Send message with file reference
|
|
544
|
+
const msg = await this.sendMessage(agentId, targetId, isImage ? '[图片]' : `[文件] ${file.originalname}`, {
|
|
545
|
+
type: isImage ? 'image' : 'file',
|
|
546
|
+
isGroup,
|
|
547
|
+
file_url: `/api/files/${fileName}`,
|
|
548
|
+
file_name: file.originalname,
|
|
549
|
+
local_path: filePath,
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
return msg;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// ─── Userfile Management ─────────────────────────────────────────
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Save an uploaded file from a user to the userfiles directory.
|
|
559
|
+
* This is called when files are received via the HTTP upload API
|
|
560
|
+
* and should be processed by the AI agent.
|
|
561
|
+
*/
|
|
562
|
+
async handleUserFileUpload(agentId, fromId, file, isGroup = false) {
|
|
563
|
+
const fileId = crypto.randomUUID();
|
|
564
|
+
const ext = path.extname(file.originalname || '.bin');
|
|
565
|
+
const timestamp = Date.now();
|
|
566
|
+
const safeName = `${timestamp}_${fileId.substring(0, 8)}${ext}`;
|
|
567
|
+
const localPath = path.join(this.userfilesDir, safeName);
|
|
568
|
+
|
|
569
|
+
fs.writeFileSync(localPath, file.buffer);
|
|
570
|
+
|
|
571
|
+
const isImage = /\.(jpg|jpeg|png|gif|webp|svg|bmp)$/i.test(ext);
|
|
572
|
+
const originalName = file.originalname || safeName;
|
|
573
|
+
|
|
574
|
+
// Save to chat history
|
|
575
|
+
const msg = this.db.saveMessage({
|
|
576
|
+
agent_id: agentId,
|
|
577
|
+
target_id: fromId,
|
|
578
|
+
from_id: fromId,
|
|
579
|
+
to_id: agentId,
|
|
580
|
+
type: isImage ? 'image' : 'file',
|
|
581
|
+
content: `[${isImage ? '图片' : '文件'}] ${originalName}`,
|
|
582
|
+
file_name: originalName,
|
|
583
|
+
local_path: localPath,
|
|
584
|
+
is_group: isGroup ? 1 : 0,
|
|
585
|
+
status: 'delivered',
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
if (this._onNewMessage) {
|
|
589
|
+
this._onNewMessage(msg);
|
|
590
|
+
|
|
591
|
+
// Inject synthetic message for AI agent
|
|
592
|
+
const fileType = isImage ? '图片' : '文件';
|
|
593
|
+
const syntheticMsg = {
|
|
594
|
+
agent_id: agentId,
|
|
595
|
+
target_id: fromId,
|
|
596
|
+
from_id: fromId,
|
|
597
|
+
to_id: agentId,
|
|
598
|
+
type: 'text',
|
|
599
|
+
content: `[用户发送了${fileType}] ${originalName}\n本地路径: ${localPath}\n文件大小: ${file.size || file.buffer?.length || 0} 字节\n请处理该${fileType}。`,
|
|
600
|
+
is_group: isGroup ? 1 : 0,
|
|
601
|
+
status: 'delivered',
|
|
602
|
+
_synthetic: true,
|
|
603
|
+
_original_msg_id: msg.message_id || msg.id,
|
|
604
|
+
};
|
|
605
|
+
this._onNewMessage(syntheticMsg);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
return { msg, localPath, originalName };
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// ─── Private Helpers ──────────────────────────────────────────────
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Check if a message represents a file/image based on type and content.
|
|
615
|
+
*/
|
|
616
|
+
_isFileMessage(msgType, content, data) {
|
|
617
|
+
// Check explicit message type
|
|
618
|
+
if (['file', 'image', 'file_chunk'].includes(msgType)) return true;
|
|
619
|
+
if (['file', 'image'].includes(data.type)) return true;
|
|
620
|
+
|
|
621
|
+
// Check for file metadata in content
|
|
622
|
+
if (typeof content === 'string') {
|
|
623
|
+
try {
|
|
624
|
+
const parsed = JSON.parse(content);
|
|
625
|
+
if (parsed.type === 'file-info' || parsed.fileId || parsed.fileName || parsed.localPath) {
|
|
626
|
+
return true;
|
|
627
|
+
}
|
|
628
|
+
} catch (e) {
|
|
629
|
+
// Not JSON
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
// Check for file_url or file data
|
|
634
|
+
if (data.file_url || data.fileUrl || data.file_data || data.fileData) return true;
|
|
635
|
+
|
|
636
|
+
// Check for known file markers in text content
|
|
637
|
+
if (typeof content === 'string' && (
|
|
638
|
+
content.startsWith('[文件]') ||
|
|
639
|
+
content.startsWith('[图片]') ||
|
|
640
|
+
content.startsWith('[File]') ||
|
|
641
|
+
content.startsWith('[Image]')
|
|
642
|
+
)) {
|
|
643
|
+
return true;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
return false;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
/**
|
|
650
|
+
* Check if a message is specifically an image (vs other file types).
|
|
651
|
+
*/
|
|
652
|
+
_isImageMessage(msgType, content, data) {
|
|
653
|
+
if (msgType === 'image' || data.type === 'image') return true;
|
|
654
|
+
|
|
655
|
+
// Check file extension in filename
|
|
656
|
+
const fileName = data.file_name || data.fileName || '';
|
|
657
|
+
if (/\.(jpg|jpeg|png|gif|webp|svg|bmp)$/i.test(fileName)) return true;
|
|
658
|
+
|
|
659
|
+
// Check content markers
|
|
660
|
+
if (typeof content === 'string' && content.startsWith('[图片]')) return true;
|
|
661
|
+
|
|
662
|
+
return false;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/**
|
|
666
|
+
* Save an incoming file to the userfiles directory.
|
|
667
|
+
* Handles various formats: inline base64, URL references, file-info JSON.
|
|
668
|
+
*/
|
|
669
|
+
_saveIncomingFileToUserfiles(agentId, fromId, content, data) {
|
|
670
|
+
try {
|
|
671
|
+
const fileId = crypto.randomUUID();
|
|
672
|
+
const timestamp = Date.now();
|
|
673
|
+
|
|
674
|
+
// Try to extract file info from the message
|
|
675
|
+
let parsed = null;
|
|
676
|
+
if (typeof content === 'string') {
|
|
677
|
+
try { parsed = JSON.parse(content); } catch (e) {}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// Case 1: file-info with chunked data (already assembled elsewhere)
|
|
681
|
+
if (parsed && parsed.localPath) {
|
|
682
|
+
// File is already on disk, just reference it
|
|
683
|
+
return {
|
|
684
|
+
localPath: parsed.localPath,
|
|
685
|
+
originalName: parsed.fileName || path.basename(parsed.localPath),
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// Case 2: Base64 data inline
|
|
690
|
+
if (data.file_data || data.fileData || (parsed && parsed.data && this._isBase64Data(parsed.data))) {
|
|
691
|
+
return this._saveBase64FileToUserfiles(agentId, fromId, {
|
|
692
|
+
...data,
|
|
693
|
+
file_data: data.file_data || data.fileData || (parsed && parsed.data),
|
|
694
|
+
file_name: data.file_name || data.fileName || (parsed && parsed.fileName) || 'file.bin',
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// Case 3: URL reference — download and save
|
|
699
|
+
if (data.file_url || data.fileUrl || (parsed && parsed.fileUrl)) {
|
|
700
|
+
return this._saveUrlFileToUserfiles(agentId, fromId, {
|
|
701
|
+
...data,
|
|
702
|
+
file_url: data.file_url || data.fileUrl || (parsed && parsed.fileUrl),
|
|
703
|
+
file_name: data.file_name || data.fileName || (parsed && parsed.fileName) || 'file.bin',
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
// Case 4: Text-based file marker like [文件] filename or [图片] filename
|
|
708
|
+
if (typeof content === 'string' && (
|
|
709
|
+
content.startsWith('[文件]') ||
|
|
710
|
+
content.startsWith('[图片]') ||
|
|
711
|
+
content.startsWith('[File]') ||
|
|
712
|
+
content.startsWith('[Image]')
|
|
713
|
+
)) {
|
|
714
|
+
const originalName = content.replace(/^\[(文件|图片|File|Image)\]\s*/, '').trim() || 'unknown';
|
|
715
|
+
const ext = path.extname(originalName) || (content.includes('图片') || content.includes('Image') ? '.png' : '.bin');
|
|
716
|
+
const safeName = `${timestamp}_${fileId.substring(0, 8)}${ext}`;
|
|
717
|
+
const localPath = path.join(this.userfilesDir, safeName);
|
|
718
|
+
|
|
719
|
+
// Create a placeholder file — the actual content may come via chunks
|
|
720
|
+
// or may already be in the uploads dir
|
|
721
|
+
const uploadsPath = path.join(this.uploadsDir, originalName);
|
|
722
|
+
if (fs.existsSync(uploadsPath)) {
|
|
723
|
+
fs.copyFileSync(uploadsPath, localPath);
|
|
724
|
+
console.log(`[Chat] Copied user file: ${originalName} -> ${localPath}`);
|
|
725
|
+
return { localPath, originalName };
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// No actual file data yet — save a placeholder
|
|
729
|
+
fs.writeFileSync(localPath, Buffer.alloc(0));
|
|
730
|
+
console.log(`[Chat] Created placeholder for user file: ${localPath}`);
|
|
731
|
+
return { localPath, originalName };
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
return null;
|
|
735
|
+
} catch (e) {
|
|
736
|
+
console.error('[Chat] Failed to save incoming file to userfiles:', e.message);
|
|
737
|
+
return null;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* Save a base64-encoded file to userfiles.
|
|
743
|
+
*/
|
|
744
|
+
_saveBase64FileToUserfiles(agentId, fromId, data) {
|
|
745
|
+
try {
|
|
746
|
+
const fileId = crypto.randomUUID();
|
|
747
|
+
const timestamp = Date.now();
|
|
748
|
+
const base64Data = data.file_data || data.fileData || data.data;
|
|
749
|
+
const originalName = data.file_name || data.fileName || 'file.bin';
|
|
750
|
+
|
|
751
|
+
if (!base64Data) return null;
|
|
752
|
+
|
|
753
|
+
// Strip data URL prefix if present (e.g., "data:image/png;base64,")
|
|
754
|
+
const base64Clean = base64Data.replace(/^data:[^;]+;base64,/, '');
|
|
755
|
+
const fileBuffer = Buffer.from(base64Clean, 'base64');
|
|
756
|
+
|
|
757
|
+
const ext = path.extname(originalName) || this._inferExtFromData(base64Data);
|
|
758
|
+
const safeName = `${timestamp}_${fileId.substring(0, 8)}${ext}`;
|
|
759
|
+
const localPath = path.join(this.userfilesDir, safeName);
|
|
760
|
+
|
|
761
|
+
fs.writeFileSync(localPath, fileBuffer);
|
|
762
|
+
console.log(`[Chat] Saved base64 user file: ${originalName} -> ${localPath} (${fileBuffer.length} bytes)`);
|
|
763
|
+
|
|
764
|
+
return { localPath, originalName };
|
|
765
|
+
} catch (e) {
|
|
766
|
+
console.error('[Chat] Failed to save base64 file:', e.message);
|
|
767
|
+
return null;
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/**
|
|
772
|
+
* Download a file from URL and save to userfiles.
|
|
773
|
+
*/
|
|
774
|
+
_saveUrlFileToUserfiles(agentId, fromId, data) {
|
|
775
|
+
try {
|
|
776
|
+
const fileId = crypto.randomUUID();
|
|
777
|
+
const timestamp = Date.now();
|
|
778
|
+
const fileUrl = data.file_url || data.fileUrl;
|
|
779
|
+
const originalName = data.file_name || data.fileName || path.basename(fileUrl || 'file.bin');
|
|
780
|
+
|
|
781
|
+
// For local server URLs, resolve the local path directly
|
|
782
|
+
if (fileUrl && fileUrl.startsWith('/api/files/')) {
|
|
783
|
+
const fileName = path.basename(fileUrl);
|
|
784
|
+
const uploadsPath = path.join(this.uploadsDir, fileName);
|
|
785
|
+
if (fs.existsSync(uploadsPath)) {
|
|
786
|
+
const ext = path.extname(originalName) || path.extname(uploadsPath);
|
|
787
|
+
const safeName = `${timestamp}_${fileId.substring(0, 8)}${ext}`;
|
|
788
|
+
const localPath = path.join(this.userfilesDir, safeName);
|
|
789
|
+
fs.copyFileSync(uploadsPath, localPath);
|
|
790
|
+
console.log(`[Chat] Copied local URL file: ${fileUrl} -> ${localPath}`);
|
|
791
|
+
return { localPath, originalName };
|
|
792
|
+
}
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
// For remote URLs, we'd need async download — log and skip for now
|
|
796
|
+
console.log(`[Chat] Remote file URL (async download not yet supported): ${fileUrl}`);
|
|
797
|
+
const ext = path.extname(originalName) || '.bin';
|
|
798
|
+
const safeName = `${timestamp}_${fileId.substring(0, 8)}${ext}`;
|
|
799
|
+
const localPath = path.join(this.userfilesDir, safeName);
|
|
800
|
+
|
|
801
|
+
// Save a placeholder with the URL reference
|
|
802
|
+
fs.writeFileSync(localPath, JSON.stringify({
|
|
803
|
+
type: 'url_reference',
|
|
804
|
+
url: fileUrl,
|
|
805
|
+
originalName,
|
|
806
|
+
timestamp,
|
|
807
|
+
}));
|
|
808
|
+
return { localPath, originalName };
|
|
809
|
+
} catch (e) {
|
|
810
|
+
console.error('[Chat] Failed to save URL file:', e.message);
|
|
811
|
+
return null;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* Check if a string looks like base64 data.
|
|
817
|
+
*/
|
|
818
|
+
_isBase64Data(str) {
|
|
819
|
+
if (typeof str !== 'string') return false;
|
|
820
|
+
if (str.startsWith('data:')) return true;
|
|
821
|
+
// Quick heuristic: long string with only base64 chars
|
|
822
|
+
if (str.length > 100 && /^[A-Za-z0-9+/=\s]+$/.test(str.substring(0, 200))) return true;
|
|
823
|
+
return false;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/**
|
|
827
|
+
* Infer file extension from base64 data URL prefix.
|
|
828
|
+
*/
|
|
829
|
+
_inferExtFromData(data) {
|
|
830
|
+
if (typeof data !== 'string') return '.bin';
|
|
831
|
+
const mimeMatch = data.match(/^data:([^;]+);/);
|
|
832
|
+
if (mimeMatch) {
|
|
833
|
+
const mime = mimeMatch[1];
|
|
834
|
+
const mimeToExt = {
|
|
835
|
+
'image/png': '.png',
|
|
836
|
+
'image/jpeg': '.jpg',
|
|
837
|
+
'image/gif': '.gif',
|
|
838
|
+
'image/webp': '.webp',
|
|
839
|
+
'image/svg+xml': '.svg',
|
|
840
|
+
'image/bmp': '.bmp',
|
|
841
|
+
'application/pdf': '.pdf',
|
|
842
|
+
'text/plain': '.txt',
|
|
843
|
+
'application/json': '.json',
|
|
844
|
+
'application/zip': '.zip',
|
|
845
|
+
'audio/mpeg': '.mp3',
|
|
846
|
+
'video/mp4': '.mp4',
|
|
847
|
+
};
|
|
848
|
+
return mimeToExt[mime] || '.bin';
|
|
849
|
+
}
|
|
850
|
+
return '.bin';
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
|
|
854
|
+
module.exports = ChatManager;
|