koishi-plugin-chat-patch 2.1.1 → 2.2.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.
- package/client/vue/chat-logic.ts +683 -678
- package/client/vue/composables/useVideoCache.ts +130 -0
- package/client/vue/index.vue +658 -612
- package/dist/index.js +2 -2
- package/lib/api-handlers.d.ts +1 -0
- package/lib/index.js +91 -20
- package/lib/utils.d.ts +13 -0
- package/package.json +52 -49
- package/src/api-handlers.ts +750 -704
- package/src/index.ts +148 -125
- package/src/message-handler.ts +413 -407
- package/src/utils.ts +163 -144
package/src/api-handlers.ts
CHANGED
|
@@ -1,704 +1,750 @@
|
|
|
1
|
-
import { FileManager } from './file-manager'
|
|
2
|
-
import { MessageHandler } from './message-handler'
|
|
3
|
-
import { Context, h, Logger, Universal } from 'koishi'
|
|
4
|
-
import { Config } from './config'
|
|
5
|
-
import { } from '@koishijs/plugin-console'
|
|
6
|
-
import { URL, pathToFileURL, fileURLToPath } from 'node:url'
|
|
7
|
-
import { readFileSync } from 'node:fs'
|
|
8
|
-
import * as mime from 'mime-types'
|
|
9
|
-
|
|
10
|
-
export class ApiHandlers {
|
|
11
|
-
private logger: Logger
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
private
|
|
17
|
-
private
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
this.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
messages =
|
|
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
|
-
|
|
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
|
-
|
|
147
|
-
const
|
|
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
|
-
|
|
177
|
-
|
|
178
|
-
const
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
this.
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
)
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
this.
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
const
|
|
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
|
-
this.
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
deletedMessages
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
this.
|
|
381
|
-
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
this.fileManager.
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
this.fileManager.
|
|
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
|
-
const
|
|
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
|
-
this.
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
return { success:
|
|
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
|
-
}
|
|
1
|
+
import { FileManager } from './file-manager'
|
|
2
|
+
import { MessageHandler } from './message-handler'
|
|
3
|
+
import { Context, h, Logger, Universal } from 'koishi'
|
|
4
|
+
import { Config } from './config'
|
|
5
|
+
import { } from '@koishijs/plugin-console'
|
|
6
|
+
import { URL, pathToFileURL, fileURLToPath } from 'node:url'
|
|
7
|
+
import { readFileSync } from 'node:fs'
|
|
8
|
+
import * as mime from 'mime-types'
|
|
9
|
+
|
|
10
|
+
export class ApiHandlers {
|
|
11
|
+
private logger: Logger
|
|
12
|
+
// 当前临时缓存的视频文件路径
|
|
13
|
+
private currentTempVideo: string | null = null
|
|
14
|
+
|
|
15
|
+
constructor(
|
|
16
|
+
private ctx: Context,
|
|
17
|
+
private config: Config,
|
|
18
|
+
private fileManager: FileManager,
|
|
19
|
+
private messageHandler: MessageHandler
|
|
20
|
+
) {
|
|
21
|
+
this.logger = ctx.logger('chat-patch')
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
registerApiHandlers() {
|
|
25
|
+
this.ctx.console.addListener('clear-all-indexeddb-data' as any, async () => {
|
|
26
|
+
try {
|
|
27
|
+
this.logInfo('收到清空 IndexedDB 数据请求')
|
|
28
|
+
// 这个 API 主要用于前端调用,后端不直接操作 IndexedDB
|
|
29
|
+
return { success: true, message: '可以清空 IndexedDB' }
|
|
30
|
+
} catch (error: any) {
|
|
31
|
+
this.logger.error('清空 IndexedDB 数据失败:', error)
|
|
32
|
+
return { success: false, error: error?.message || String(error) }
|
|
33
|
+
}
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
// 获取所有聊天数据的 API
|
|
37
|
+
this.ctx.console.addListener('get-chat-data' as any, async () => {
|
|
38
|
+
try {
|
|
39
|
+
// 优化性能:只读取基础信息,不读取庞大的消息体
|
|
40
|
+
const data = this.fileManager.readChatDataFromFile()
|
|
41
|
+
|
|
42
|
+
this.logInfo('获取基础聊天数据')
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
success: true,
|
|
46
|
+
data: {
|
|
47
|
+
bots: data.bots || {},
|
|
48
|
+
channels: data.channels || {},
|
|
49
|
+
pinnedBots: data.pinnedBots || [],
|
|
50
|
+
pinnedChannels: data.pinnedChannels || [],
|
|
51
|
+
// 不返回 messages,由前端按需拉取
|
|
52
|
+
messages: {}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
} catch (error: any) {
|
|
56
|
+
this.logger.error('获取聊天数据失败:', error)
|
|
57
|
+
return { success: false, error: error?.message || String(error) }
|
|
58
|
+
}
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
// 获取历史消息的 API
|
|
62
|
+
this.ctx.console.addListener('get-history-messages' as any, async (requestData: {
|
|
63
|
+
selfId: string
|
|
64
|
+
channelId: string
|
|
65
|
+
limit?: number
|
|
66
|
+
offset?: number
|
|
67
|
+
}) => {
|
|
68
|
+
try {
|
|
69
|
+
const data = this.fileManager.readChatDataFromFile()
|
|
70
|
+
const channelKey = `${requestData.selfId}:${requestData.channelId}`
|
|
71
|
+
let messages = data.messages[channelKey] || []
|
|
72
|
+
|
|
73
|
+
// 按时间戳降序排列(最新的消息在前面)
|
|
74
|
+
const sortedMessages = messages.sort((a, b) => b.timestamp - a.timestamp)
|
|
75
|
+
|
|
76
|
+
// 如果提供了分页参数,则进行分页处理
|
|
77
|
+
if (requestData.limit !== undefined) {
|
|
78
|
+
const limit = requestData.limit
|
|
79
|
+
const offset = requestData.offset || 0
|
|
80
|
+
messages = sortedMessages.slice(offset, offset + limit)
|
|
81
|
+
// 重新按时间戳升序排列,以保持消息显示顺序
|
|
82
|
+
messages = messages.sort((a, b) => a.timestamp - b.timestamp)
|
|
83
|
+
} else {
|
|
84
|
+
// 默认返回所有消息,按时间戳升序排列
|
|
85
|
+
messages = sortedMessages.sort((a, b) => a.timestamp - b.timestamp)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
this.logInfo('获取历史消息:', channelKey, '共', messages.length, '条消息')
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
success: true,
|
|
92
|
+
messages: messages,
|
|
93
|
+
total: sortedMessages.length
|
|
94
|
+
}
|
|
95
|
+
} catch (error: any) {
|
|
96
|
+
this.logger.error('获取历史消息失败:', error)
|
|
97
|
+
return { success: false, error: error?.message || String(error), messages: [], total: 0 }
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
// 获取所有频道消息数量的 API
|
|
102
|
+
this.ctx.console.addListener('get-all-channel-message-counts' as any, async () => {
|
|
103
|
+
try {
|
|
104
|
+
const data = this.fileManager.readChatDataFromFile()
|
|
105
|
+
const counts: Record<string, number> = {}
|
|
106
|
+
|
|
107
|
+
for (const [channelKey, messages] of Object.entries(data.messages)) {
|
|
108
|
+
counts[channelKey] = messages.length
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
this.logInfo('获取所有频道消息数量:', {
|
|
112
|
+
频道数: Object.keys(counts).length,
|
|
113
|
+
总消息数: Object.values(counts).reduce((total, count) => total + count, 0)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
success: true,
|
|
118
|
+
counts: counts
|
|
119
|
+
}
|
|
120
|
+
} catch (error: any) {
|
|
121
|
+
this.logger.error('获取频道消息数量失败:', error)
|
|
122
|
+
return { success: false, error: error?.message || String(error), counts: {} }
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
// 图片获取 API
|
|
127
|
+
this.ctx.console.addListener('fetch-image' as any, async (data: { url: string }) => {
|
|
128
|
+
try {
|
|
129
|
+
// 检查是否是本地文件路径
|
|
130
|
+
if (this.isFileUrl(data.url)) {
|
|
131
|
+
this.logInfo('处理本地文件请求:', data.url)
|
|
132
|
+
return await this.handleLocalFileRequest(data.url)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 处理网络图片
|
|
136
|
+
const response = await fetch(data.url, {
|
|
137
|
+
headers: {
|
|
138
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
|
139
|
+
'Referer': ''
|
|
140
|
+
}
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
if (!response.ok) {
|
|
144
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const buffer = await response.arrayBuffer()
|
|
148
|
+
const base64 = Buffer.from(buffer).toString('base64')
|
|
149
|
+
const contentType = response.headers.get('content-type') || 'image/jpeg'
|
|
150
|
+
return {
|
|
151
|
+
success: true,
|
|
152
|
+
base64: base64,
|
|
153
|
+
contentType: contentType,
|
|
154
|
+
dataUrl: `data:${contentType};base64,${base64}`
|
|
155
|
+
}
|
|
156
|
+
} catch (error: any) {
|
|
157
|
+
return { success: false, error: error?.message || String(error) }
|
|
158
|
+
}
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
// 清理频道历史记录的 API
|
|
162
|
+
this.ctx.console.addListener('clear-channel-history' as any, async (data: {
|
|
163
|
+
selfId: string
|
|
164
|
+
channelId: string
|
|
165
|
+
keepCount?: number
|
|
166
|
+
}) => {
|
|
167
|
+
try {
|
|
168
|
+
this.logInfo('收到清理历史记录请求:', data)
|
|
169
|
+
|
|
170
|
+
const chatData = this.fileManager.readChatDataFromFile()
|
|
171
|
+
const channelKey = `${data.selfId}:${data.channelId}`
|
|
172
|
+
|
|
173
|
+
if (!chatData.messages[channelKey]) {
|
|
174
|
+
return { success: true, message: '频道没有历史消息' }
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const messages = chatData.messages[channelKey]
|
|
178
|
+
const originalCount = messages.length
|
|
179
|
+
|
|
180
|
+
const keepCount = data.keepCount || this.config.keepMessagesOnClear
|
|
181
|
+
|
|
182
|
+
if (keepCount > 0 && originalCount <= keepCount) {
|
|
183
|
+
return { success: true, message: `消息数量(${originalCount})未超过保留数量(${keepCount}),无需清理` }
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const sortedMessages = [...messages].sort((a, b) => a.timestamp - b.timestamp)
|
|
187
|
+
const keptMessages = keepCount > 0 ? sortedMessages.slice(-keepCount) : []
|
|
188
|
+
const clearedCount = originalCount - keptMessages.length
|
|
189
|
+
|
|
190
|
+
chatData.messages[channelKey] = keptMessages
|
|
191
|
+
this.fileManager.writeChatDataToFile(chatData)
|
|
192
|
+
|
|
193
|
+
this.logInfo(`频道 ${channelKey} 历史记录清理完成:`, {
|
|
194
|
+
原始消息数: originalCount,
|
|
195
|
+
保留消息数: keptMessages.length,
|
|
196
|
+
清理消息数: clearedCount
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
success: true,
|
|
201
|
+
message: `成功清理 ${clearedCount} 条历史消息,保留最新 ${keptMessages.length} 条`,
|
|
202
|
+
clearedCount: clearedCount,
|
|
203
|
+
keptCount: keptMessages.length
|
|
204
|
+
}
|
|
205
|
+
} catch (error: any) {
|
|
206
|
+
this.logger.error('清理频道历史记录失败:', error)
|
|
207
|
+
return { success: false, error: error?.message || String(error) }
|
|
208
|
+
}
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
// 发送消息的 API
|
|
212
|
+
this.ctx.console.addListener('send-message' as any, async (data: {
|
|
213
|
+
selfId: string
|
|
214
|
+
channelId: string
|
|
215
|
+
content: string
|
|
216
|
+
images?: Array<{
|
|
217
|
+
tempId: string
|
|
218
|
+
filename: string
|
|
219
|
+
}>
|
|
220
|
+
}) => {
|
|
221
|
+
try {
|
|
222
|
+
this.logInfo('收到发送消息请求:', data)
|
|
223
|
+
|
|
224
|
+
// 优化机器人查找逻辑
|
|
225
|
+
const bot = this.ctx.bots.find((bot: any) => bot.selfId === data.selfId || bot.user?.id === data.selfId)
|
|
226
|
+
if (!bot) {
|
|
227
|
+
this.logger.error('未找到机器人:', data.selfId, '当前可用机器人:', this.ctx.bots.map((b: any) => b.selfId))
|
|
228
|
+
return { success: false, error: `未找到机器人 ${data.selfId},请检查机器人是否在线` }
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// 检查机器人状态
|
|
232
|
+
if (bot.status !== Universal.Status.ONLINE) {
|
|
233
|
+
this.logger.error('机器人离线:', data.selfId, '状态:', bot.status)
|
|
234
|
+
return { success: false, error: `机器人 ${data.selfId} 当前离线` }
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
let messageContent = data.content
|
|
238
|
+
|
|
239
|
+
// 如果有图片,添加图片元素
|
|
240
|
+
if (data.images && data.images.length > 0) {
|
|
241
|
+
const tempDir = this.ctx.baseDir + '/data/chat-patch/temp'
|
|
242
|
+
|
|
243
|
+
for (const image of data.images) {
|
|
244
|
+
const files = require('fs').readdirSync(tempDir).filter((file: string) =>
|
|
245
|
+
file.includes(`temp_${image.tempId}`)
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
if (files.length > 0) {
|
|
249
|
+
const imagePath = `${tempDir}/${files[0]}`
|
|
250
|
+
// 使用 pathToFileURL 创建正确的文件 URL
|
|
251
|
+
const fileUrl = this.createFileUrl(imagePath)
|
|
252
|
+
messageContent += h.image(fileUrl).toString()
|
|
253
|
+
this.logInfo('添加图片到消息:', { imagePath, fileUrl })
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const parsedContent = h.parse(messageContent)
|
|
259
|
+
|
|
260
|
+
// 在发送消息前,通知 MessageHandler 正确的 channelId
|
|
261
|
+
this.messageHandler.setCorrectChannelId(data.selfId, data.channelId)
|
|
262
|
+
|
|
263
|
+
const result = await bot.sendMessage(data.channelId, parsedContent)
|
|
264
|
+
this.logInfo('消息发送成功:', result)
|
|
265
|
+
|
|
266
|
+
const messageId = Array.isArray(result) ? result[0] : result;
|
|
267
|
+
|
|
268
|
+
// 发送成功后,更新本地存储中的虚拟消息为真实 ID
|
|
269
|
+
if (messageId) {
|
|
270
|
+
const chatData = this.fileManager.readChatDataFromFile()
|
|
271
|
+
const channelKey = `${data.selfId}:${data.channelId}`
|
|
272
|
+
const messages = chatData.messages[channelKey] || []
|
|
273
|
+
// 找到最近的一条正在发送的机器人消息
|
|
274
|
+
const msg = [...messages].reverse().find(m => m.type === 'bot' && m.sending)
|
|
275
|
+
if (msg) {
|
|
276
|
+
msg.realId = messageId;
|
|
277
|
+
msg.sending = false;
|
|
278
|
+
this.fileManager.writeChatDataToFile(chatData)
|
|
279
|
+
|
|
280
|
+
// 广播更新事件给前端
|
|
281
|
+
this.ctx.console.broadcast('bot-message-updated', {
|
|
282
|
+
channelKey,
|
|
283
|
+
tempId: msg.id,
|
|
284
|
+
realId: messageId
|
|
285
|
+
})
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
success: !!messageId,
|
|
291
|
+
messageId: messageId,
|
|
292
|
+
tempImageIds: data.images?.map(img => img.tempId) || []
|
|
293
|
+
}
|
|
294
|
+
} catch (error: any) {
|
|
295
|
+
this.logger.error('发送消息失败:', error)
|
|
296
|
+
return { success: false, error: error?.message || String(error) }
|
|
297
|
+
}
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
// 清理临时图片的 API(由前端在消息发送成功后调用)
|
|
301
|
+
this.ctx.console.addListener('cleanup-temp-images' as any, async (data: {
|
|
302
|
+
tempImageIds: string[]
|
|
303
|
+
}) => {
|
|
304
|
+
try {
|
|
305
|
+
this.logInfo('收到清理临时图片请求:', data.tempImageIds)
|
|
306
|
+
|
|
307
|
+
// 不立即删除,只是记录日志,让定时任务处理清理
|
|
308
|
+
this.logInfo('临时图片将由定时任务清理,保持文件可用性')
|
|
309
|
+
|
|
310
|
+
return { success: true, cleanedCount: 0 }
|
|
311
|
+
} catch (error: any) {
|
|
312
|
+
this.logger.error('清理临时图片失败:', error)
|
|
313
|
+
return { success: false, error: error?.message || String(error) }
|
|
314
|
+
}
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
// 删除机器人所有数据的 API
|
|
318
|
+
this.ctx.console.addListener('delete-bot-data' as any, async (data: {
|
|
319
|
+
selfId: string
|
|
320
|
+
}) => {
|
|
321
|
+
try {
|
|
322
|
+
this.logInfo('收到删除机器人数据请求:', data)
|
|
323
|
+
|
|
324
|
+
const chatData = this.fileManager.readChatDataFromFile()
|
|
325
|
+
let deletedChannels = 0
|
|
326
|
+
let deletedMessages = 0
|
|
327
|
+
|
|
328
|
+
if (chatData.channels[data.selfId]) {
|
|
329
|
+
deletedChannels = Object.keys(chatData.channels[data.selfId]).length
|
|
330
|
+
delete chatData.channels[data.selfId]
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const channelsToDelete = Object.keys(chatData.messages).filter(key => key.startsWith(`${data.selfId}:`))
|
|
334
|
+
for (const channelKey of channelsToDelete) {
|
|
335
|
+
deletedMessages += chatData.messages[channelKey].length
|
|
336
|
+
delete chatData.messages[channelKey]
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
delete chatData.bots[data.selfId]
|
|
340
|
+
this.fileManager.writeChatDataToFile(chatData)
|
|
341
|
+
|
|
342
|
+
this.logInfo(`机器人 ${data.selfId} 数据删除完成:`, {
|
|
343
|
+
删除频道数: deletedChannels,
|
|
344
|
+
删除消息数: deletedMessages
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
return {
|
|
348
|
+
success: true,
|
|
349
|
+
message: `成功删除机器人数据:${deletedChannels} 个频道,${deletedMessages} 条消息`,
|
|
350
|
+
deletedChannels,
|
|
351
|
+
deletedMessages
|
|
352
|
+
}
|
|
353
|
+
} catch (error: any) {
|
|
354
|
+
this.logger.error('删除机器人数据失败:', error)
|
|
355
|
+
return { success: false, error: error?.message || String(error) }
|
|
356
|
+
}
|
|
357
|
+
})
|
|
358
|
+
|
|
359
|
+
// 删除频道所有数据的 API
|
|
360
|
+
this.ctx.console.addListener('delete-channel-data' as any, async (data: {
|
|
361
|
+
selfId: string
|
|
362
|
+
channelId: string
|
|
363
|
+
}) => {
|
|
364
|
+
try {
|
|
365
|
+
this.logInfo('收到删除频道数据请求:', data)
|
|
366
|
+
|
|
367
|
+
const chatData = this.fileManager.readChatDataFromFile()
|
|
368
|
+
const channelKey = `${data.selfId}:${data.channelId}`
|
|
369
|
+
let deletedMessages = 0
|
|
370
|
+
|
|
371
|
+
if (chatData.messages[channelKey]) {
|
|
372
|
+
deletedMessages = chatData.messages[channelKey].length
|
|
373
|
+
delete chatData.messages[channelKey]
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (chatData.channels[data.selfId] && chatData.channels[data.selfId][data.channelId]) {
|
|
377
|
+
delete chatData.channels[data.selfId][data.channelId]
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
this.fileManager.writeChatDataToFile(chatData)
|
|
381
|
+
|
|
382
|
+
this.logInfo(`频道 ${channelKey} 数据删除完成:`, {
|
|
383
|
+
删除消息数: deletedMessages
|
|
384
|
+
})
|
|
385
|
+
|
|
386
|
+
return {
|
|
387
|
+
success: true,
|
|
388
|
+
message: `成功删除频道数据:${deletedMessages} 条消息`,
|
|
389
|
+
deletedMessages
|
|
390
|
+
}
|
|
391
|
+
} catch (error: any) {
|
|
392
|
+
this.logger.error('删除频道数据失败:', error)
|
|
393
|
+
return { success: false, error: error?.message || String(error) }
|
|
394
|
+
}
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
// 设置置顶机器人列表的 API
|
|
398
|
+
this.ctx.console.addListener('set-pinned-bots' as any, async (data: {
|
|
399
|
+
pinnedBots: string[]
|
|
400
|
+
}) => {
|
|
401
|
+
try {
|
|
402
|
+
this.logInfo('收到设置置顶机器人请求:', data.pinnedBots)
|
|
403
|
+
const chatData = this.fileManager.readChatDataFromFile()
|
|
404
|
+
chatData.pinnedBots = data.pinnedBots
|
|
405
|
+
this.fileManager.writeChatDataToFile(chatData)
|
|
406
|
+
return { success: true }
|
|
407
|
+
} catch (error: any) {
|
|
408
|
+
this.logger.error('设置置顶机器人失败:', error)
|
|
409
|
+
return { success: false, error: error?.message || String(error) }
|
|
410
|
+
}
|
|
411
|
+
})
|
|
412
|
+
|
|
413
|
+
// 设置置顶频道列表的 API
|
|
414
|
+
this.ctx.console.addListener('set-pinned-channels' as any, async (data: {
|
|
415
|
+
pinnedChannels: string[]
|
|
416
|
+
}) => {
|
|
417
|
+
try {
|
|
418
|
+
this.logInfo('收到设置置顶频道请求:', data.pinnedChannels)
|
|
419
|
+
const chatData = this.fileManager.readChatDataFromFile()
|
|
420
|
+
chatData.pinnedChannels = data.pinnedChannels
|
|
421
|
+
this.fileManager.writeChatDataToFile(chatData)
|
|
422
|
+
return { success: true }
|
|
423
|
+
} catch (error: any) {
|
|
424
|
+
this.logger.error('设置置顶频道失败:', error)
|
|
425
|
+
return { success: false, error: error?.message || String(error) }
|
|
426
|
+
}
|
|
427
|
+
})
|
|
428
|
+
|
|
429
|
+
// 上传图片的 API
|
|
430
|
+
this.ctx.console.addListener('upload-image' as any, async (data: {
|
|
431
|
+
file: string // base64 encoded image
|
|
432
|
+
filename: string
|
|
433
|
+
mimeType: string
|
|
434
|
+
isGif?: boolean // 是否为GIF图片
|
|
435
|
+
}) => {
|
|
436
|
+
try {
|
|
437
|
+
this.logInfo('收到图片上传请求:', { filename: data.filename, mimeType: data.mimeType, isGif: data.isGif })
|
|
438
|
+
|
|
439
|
+
// 将base64转换为Buffer
|
|
440
|
+
const base64Data = data.file.replace(/^data:image\/\w+;base64,/, '')
|
|
441
|
+
const buffer = Buffer.from(base64Data, 'base64')
|
|
442
|
+
|
|
443
|
+
// 生成临时文件路径,保持原始扩展名以保持GIF格式
|
|
444
|
+
const tempId = Date.now() + '_' + Math.random().toString(36).substring(2, 11)
|
|
445
|
+
const extension = data.filename.split('.').pop()?.toLowerCase() || (data.isGif ? 'gif' : 'jpg')
|
|
446
|
+
const tempFilename = `temp_${tempId}.${extension}`
|
|
447
|
+
|
|
448
|
+
// 保存到临时目录
|
|
449
|
+
const tempDir = this.ctx.baseDir + '/data/chat-patch/temp'
|
|
450
|
+
if (!require('fs').existsSync(tempDir)) {
|
|
451
|
+
require('fs').mkdirSync(tempDir, { recursive: true })
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const tempPath = `${tempDir}/${tempFilename}`
|
|
455
|
+
|
|
456
|
+
// 对于GIF文件,直接保存原始数据以保持动画
|
|
457
|
+
require('fs').writeFileSync(tempPath, buffer)
|
|
458
|
+
|
|
459
|
+
this.logInfo('图片上传成功:', { tempPath, size: buffer.length, isGif: data.isGif })
|
|
460
|
+
|
|
461
|
+
return {
|
|
462
|
+
success: true,
|
|
463
|
+
tempId: tempId,
|
|
464
|
+
tempPath: tempPath,
|
|
465
|
+
filename: data.filename,
|
|
466
|
+
size: buffer.length,
|
|
467
|
+
isGif: data.isGif
|
|
468
|
+
}
|
|
469
|
+
} catch (error: any) {
|
|
470
|
+
this.logger.error('图片上传失败:', error)
|
|
471
|
+
return { success: false, error: error?.message || String(error) }
|
|
472
|
+
}
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
// 删除临时图片的 API
|
|
476
|
+
this.ctx.console.addListener('delete-temp-image' as any, async (data: {
|
|
477
|
+
tempId: string
|
|
478
|
+
}) => {
|
|
479
|
+
try {
|
|
480
|
+
const tempDir = this.ctx.baseDir + '/data/chat-patch/temp'
|
|
481
|
+
const files = require('fs').readdirSync(tempDir).filter((file: string) =>
|
|
482
|
+
file.includes(`temp_${data.tempId}`)
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
for (const file of files) {
|
|
486
|
+
const filePath = `${tempDir}/${file}`
|
|
487
|
+
if (require('fs').existsSync(filePath)) {
|
|
488
|
+
require('fs').unlinkSync(filePath)
|
|
489
|
+
this.logInfo('删除临时图片:', filePath)
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
return { success: true }
|
|
494
|
+
} catch (error: any) {
|
|
495
|
+
this.logger.error('删除临时图片失败:', error)
|
|
496
|
+
return { success: false, error: error?.message || String(error) }
|
|
497
|
+
}
|
|
498
|
+
})
|
|
499
|
+
|
|
500
|
+
// 定时清理过期临时文件(备用机制)
|
|
501
|
+
this.setupTempFileCleanup()
|
|
502
|
+
|
|
503
|
+
// 获取插件配置的 API
|
|
504
|
+
this.ctx.console.addListener('get-plugin-config' as any, async () => {
|
|
505
|
+
try {
|
|
506
|
+
return {
|
|
507
|
+
success: true,
|
|
508
|
+
config: {
|
|
509
|
+
maxMessagesPerChannel: this.config.maxMessagesPerChannel,
|
|
510
|
+
keepMessagesOnClear: this.config.keepMessagesOnClear,
|
|
511
|
+
maxPersistImages: this.config.maxPersistImages,
|
|
512
|
+
loggerinfo: this.config.loggerinfo,
|
|
513
|
+
blockedPlatforms: this.config.blockedPlatforms || [],
|
|
514
|
+
clearIndexedDBOnStart: this.config.clearIndexedDBOnStart
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
} catch (error: any) {
|
|
518
|
+
this.logger.error('获取插件配置失败:', error)
|
|
519
|
+
return { success: false, error: error?.message || String(error) }
|
|
520
|
+
}
|
|
521
|
+
})
|
|
522
|
+
|
|
523
|
+
// 获取用户信息 API
|
|
524
|
+
this.ctx.console.addListener('get-user-info' as any, async (data: { selfId: string, userId: string, guildId?: string }) => {
|
|
525
|
+
try {
|
|
526
|
+
const bot = this.ctx.bots.find(b => b.selfId === data.selfId)
|
|
527
|
+
if (!bot) return { success: false, error: '机器人不存在' }
|
|
528
|
+
|
|
529
|
+
// 检查平台是否支持 getUser 方法
|
|
530
|
+
if (!bot.getUser || typeof bot.getUser !== 'function') {
|
|
531
|
+
return { success: false, error: '此平台不支持查看用户信息' }
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
const user = await bot.getUser(data.userId, data.guildId)
|
|
535
|
+
|
|
536
|
+
if (user) {
|
|
537
|
+
// 缓存头像到本地
|
|
538
|
+
if (user.avatar) {
|
|
539
|
+
user.avatar = await this.messageHandler.downloadAndCacheMedia(user.avatar, 'avatar')
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const chatData = this.fileManager.readChatDataFromFile()
|
|
543
|
+
let changed = false
|
|
544
|
+
|
|
545
|
+
// 1. 如果是私聊,更新频道名 - 支持多种私聊ID格式
|
|
546
|
+
const botChannels = chatData.channels[data.selfId] || {}
|
|
547
|
+
|
|
548
|
+
// 查找所有可能的私聊频道ID格式
|
|
549
|
+
const possibleChannelIds = [
|
|
550
|
+
data.userId,
|
|
551
|
+
`private:${data.userId}`,
|
|
552
|
+
`direct:${data.userId}`
|
|
553
|
+
]
|
|
554
|
+
|
|
555
|
+
for (const channelId of possibleChannelIds) {
|
|
556
|
+
const channel = botChannels[channelId]
|
|
557
|
+
if (channel && channel.isDirect) {
|
|
558
|
+
const newName = `私聊(${user.name})`
|
|
559
|
+
if (channel.name !== newName) {
|
|
560
|
+
channel.name = newName
|
|
561
|
+
changed = true
|
|
562
|
+
this.logInfo('更新私聊频道名称:', { channelId, oldName: channel.name, newName })
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// 2. 更新该用户在所有历史消息中的头像和昵称(持久化缓存)
|
|
568
|
+
const channelKeyPrefix = `${data.selfId}:`
|
|
569
|
+
for (const [key, messages] of Object.entries(chatData.messages)) {
|
|
570
|
+
if (key.startsWith(channelKeyPrefix)) {
|
|
571
|
+
messages.forEach(msg => {
|
|
572
|
+
if (msg.userId === data.userId) {
|
|
573
|
+
if (user.name && msg.username !== user.name) {
|
|
574
|
+
msg.username = user.name
|
|
575
|
+
changed = true
|
|
576
|
+
}
|
|
577
|
+
if (user.avatar && msg.avatar !== user.avatar) {
|
|
578
|
+
msg.avatar = user.avatar
|
|
579
|
+
changed = true
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
if (changed) {
|
|
587
|
+
this.fileManager.writeChatDataToFile(chatData)
|
|
588
|
+
// 广播更新事件,让前端实时刷新
|
|
589
|
+
this.ctx.console.broadcast('chat-data-updated', {})
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
return { success: true, data: user }
|
|
594
|
+
} catch (error: any) {
|
|
595
|
+
return { success: false, error: error?.message || '获取用户信息失败' }
|
|
596
|
+
}
|
|
597
|
+
})
|
|
598
|
+
|
|
599
|
+
// 调试API:获取原始文件数据
|
|
600
|
+
this.ctx.console.addListener('debug-get-raw-data' as any, async () => {
|
|
601
|
+
try {
|
|
602
|
+
const data = this.fileManager.readChatDataFromFile()
|
|
603
|
+
return {
|
|
604
|
+
success: true,
|
|
605
|
+
data: data
|
|
606
|
+
}
|
|
607
|
+
} catch (error: any) {
|
|
608
|
+
this.logger.error('获取原始数据失败:', error)
|
|
609
|
+
return { success: false, error: error?.message || String(error) }
|
|
610
|
+
}
|
|
611
|
+
})
|
|
612
|
+
|
|
613
|
+
// 按需加载视频的临时缓存 API - 返回 base64 供前端转换为 blob
|
|
614
|
+
this.ctx.console.addListener('fetch-video-temp' as any, async (data: { url: string }) => {
|
|
615
|
+
try {
|
|
616
|
+
this.logInfo('收到视频临时加载请求:', data.url)
|
|
617
|
+
|
|
618
|
+
// 检查是否是本地文件
|
|
619
|
+
if (this.isFileUrl(data.url)) {
|
|
620
|
+
const result = await this.handleLocalFileRequest(data.url)
|
|
621
|
+
return result
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// 下载视频并返回 base64
|
|
625
|
+
const response = await fetch(data.url, {
|
|
626
|
+
headers: {
|
|
627
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
|
628
|
+
'Referer': ''
|
|
629
|
+
}
|
|
630
|
+
})
|
|
631
|
+
|
|
632
|
+
if (!response.ok) {
|
|
633
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const buffer = await response.arrayBuffer()
|
|
637
|
+
const base64 = Buffer.from(buffer).toString('base64')
|
|
638
|
+
|
|
639
|
+
// 尝试从 Content-Type 获取 MIME 类型
|
|
640
|
+
const contentType = response.headers.get('content-type') || 'video/mp4'
|
|
641
|
+
|
|
642
|
+
this.logInfo('视频下载成功:', { size: buffer.byteLength, contentType })
|
|
643
|
+
|
|
644
|
+
return {
|
|
645
|
+
success: true,
|
|
646
|
+
base64: base64,
|
|
647
|
+
contentType: contentType,
|
|
648
|
+
dataUrl: `data:${contentType};base64,${base64}`,
|
|
649
|
+
size: buffer.byteLength
|
|
650
|
+
}
|
|
651
|
+
} catch (error: any) {
|
|
652
|
+
this.logger.error('视频临时加载失败:', error)
|
|
653
|
+
return { success: false, error: error?.message || String(error) }
|
|
654
|
+
}
|
|
655
|
+
})
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// 检查是否为文件 URL
|
|
659
|
+
private isFileUrl(url: string): boolean {
|
|
660
|
+
try {
|
|
661
|
+
const parsedUrl = new URL(url)
|
|
662
|
+
return parsedUrl.protocol === 'file:'
|
|
663
|
+
} catch {
|
|
664
|
+
return false
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// 创建文件 URL
|
|
669
|
+
private createFileUrl(filePath: string): string {
|
|
670
|
+
try {
|
|
671
|
+
return pathToFileURL(filePath).href
|
|
672
|
+
} catch (error) {
|
|
673
|
+
this.logger.error('创建文件URL失败:', { filePath, error })
|
|
674
|
+
// 回退到简单的字符串拼接
|
|
675
|
+
return `file://${filePath}`
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// 处理本地文件请求
|
|
680
|
+
private async handleLocalFileRequest(fileUrl: string) {
|
|
681
|
+
try {
|
|
682
|
+
// 使用 fileURLToPath 转换 file:// URL 为系统路径
|
|
683
|
+
const filePath = fileURLToPath(fileUrl)
|
|
684
|
+
|
|
685
|
+
// 改用 node:fs 直接读取文件,避免 ctx.http.file 可能存在的编码或协议处理问题
|
|
686
|
+
const buffer = readFileSync(filePath)
|
|
687
|
+
const base64 = buffer.toString('base64')
|
|
688
|
+
|
|
689
|
+
// 使用 mime-types 库精确推断 MIME 类型
|
|
690
|
+
const contentType = mime.lookup(filePath) || 'application/octet-stream'
|
|
691
|
+
|
|
692
|
+
this.logInfo('成功读取本地文件:', { fileUrl, filePath, contentType })
|
|
693
|
+
|
|
694
|
+
return {
|
|
695
|
+
success: true,
|
|
696
|
+
base64: base64,
|
|
697
|
+
contentType: contentType,
|
|
698
|
+
dataUrl: `data:${contentType};base64,${base64}`
|
|
699
|
+
}
|
|
700
|
+
} catch (error: any) {
|
|
701
|
+
this.logger.error('读取本地文件失败:', { fileUrl, error: error.message })
|
|
702
|
+
return {
|
|
703
|
+
success: false,
|
|
704
|
+
error: `读取本地文件失败: ${error.message}`
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// 设置定时清理临时文件
|
|
710
|
+
private setupTempFileCleanup() {
|
|
711
|
+
// 每5分钟执行一次基于数量的清理
|
|
712
|
+
setInterval(() => {
|
|
713
|
+
this.cleanupMediaCache()
|
|
714
|
+
}, 5 * 60 * 1000)
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// 统一清理媒体缓存
|
|
718
|
+
private async cleanupMediaCache() {
|
|
719
|
+
const baseDir = this.ctx.baseDir + '/data/chat-patch/persist-media'
|
|
720
|
+
if (!require('node:fs').existsSync(baseDir)) return
|
|
721
|
+
|
|
722
|
+
const cleanupDir = (dirName: string, limit: number) => {
|
|
723
|
+
const dirPath = require('node:path').join(baseDir, dirName)
|
|
724
|
+
if (!require('node:fs').existsSync(dirPath)) return
|
|
725
|
+
|
|
726
|
+
const files = require('node:fs').readdirSync(dirPath)
|
|
727
|
+
.map(file => {
|
|
728
|
+
const filePath = require('node:path').join(dirPath, file)
|
|
729
|
+
const stats = require('node:fs').statSync(filePath)
|
|
730
|
+
return { name: file, path: filePath, mtime: stats.mtimeMs }
|
|
731
|
+
})
|
|
732
|
+
.sort((a, b) => b.mtime - a.mtime)
|
|
733
|
+
|
|
734
|
+
if (files.length > limit) {
|
|
735
|
+
files.slice(limit).forEach(f => {
|
|
736
|
+
try { require('node:fs').unlinkSync(f.path) } catch { }
|
|
737
|
+
})
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
cleanupDir('images', 100) // 图片缓存100个
|
|
742
|
+
cleanupDir('media', 20) // 富媒体缓存20个
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
private logInfo(...args: any[]) {
|
|
746
|
+
if (this.config.loggerinfo) {
|
|
747
|
+
(this.logger.info as (...args: any[]) => void)(...args)
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
}
|