@goodandready/dsh-messenger-gateway 0.3.21 → 0.3.22
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/lib/adapters/discord.js +8 -0
- package/lib/adapters/slack.js +6 -0
- package/lib/adapters/telegram.js +712 -708
- package/lib/client.js +320 -1
- package/lib/commands.js +74 -73
- package/lib/config.js +104 -104
- package/lib/content-guard.js +14 -14
- package/lib/gateway.js +1772 -1737
- package/lib/index.js +532 -500
- package/lib/locales/en.js +123 -114
- package/lib/locales/index.js +21 -21
- package/lib/locales/zh.js +123 -114
- package/lib/scheduler.js +159 -159
- package/lib/telegram-errors.js +0 -0
- package/lib/text.js +39 -39
- package/lib/updater.js +304 -0
- package/package.json +1 -1
package/lib/gateway.js
CHANGED
|
@@ -1,1737 +1,1772 @@
|
|
|
1
|
-
import { readFile } from 'node:fs/promises'
|
|
2
|
-
import { randomUUID } from 'node:crypto'
|
|
3
|
-
import { join } from 'node:path'
|
|
4
|
-
import { homedir } from 'node:os'
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import {
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
import {
|
|
18
|
-
import {
|
|
19
|
-
import {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
28
|
-
import {
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
import {
|
|
37
|
-
import {
|
|
38
|
-
import {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
this.
|
|
65
|
-
this.
|
|
66
|
-
this.
|
|
67
|
-
this.
|
|
68
|
-
this.
|
|
69
|
-
this.
|
|
70
|
-
this.
|
|
71
|
-
this.
|
|
72
|
-
this.
|
|
73
|
-
this.
|
|
74
|
-
this.
|
|
75
|
-
this.
|
|
76
|
-
|
|
77
|
-
this.
|
|
78
|
-
|
|
79
|
-
this.
|
|
80
|
-
this.
|
|
81
|
-
this.
|
|
82
|
-
this.
|
|
83
|
-
this.
|
|
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
|
-
if (
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
chat
|
|
137
|
-
|
|
138
|
-
chat.
|
|
139
|
-
|
|
140
|
-
chat.
|
|
141
|
-
run.
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
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
|
-
const
|
|
176
|
-
if (home
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
const
|
|
185
|
-
if (
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
this.
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
this.
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
collector.
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
collector.
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
this.
|
|
243
|
-
|
|
244
|
-
this.ctx.logger?.
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
this.
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
if (this.
|
|
263
|
-
|
|
264
|
-
this.
|
|
265
|
-
this.
|
|
266
|
-
|
|
267
|
-
this.
|
|
268
|
-
this.
|
|
269
|
-
this.
|
|
270
|
-
this.
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
this.
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
this.stats.
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
const
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
let
|
|
312
|
-
let
|
|
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
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
const
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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
|
-
const
|
|
455
|
-
const
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
const
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
const
|
|
476
|
-
const
|
|
477
|
-
const
|
|
478
|
-
const
|
|
479
|
-
|
|
480
|
-
const
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
const
|
|
494
|
-
await
|
|
495
|
-
const
|
|
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
|
-
this.
|
|
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
|
-
const
|
|
562
|
-
|
|
563
|
-
const
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
const
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
const
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
if (
|
|
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
|
-
chat.
|
|
616
|
-
|
|
617
|
-
chat.
|
|
618
|
-
|
|
619
|
-
run.catch((
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
const
|
|
643
|
-
const
|
|
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
|
-
return reply(t('persona.
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
const
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
const
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
const
|
|
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
|
-
|
|
778
|
-
|
|
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
|
-
return reply(
|
|
818
|
-
}
|
|
819
|
-
|
|
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
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
:
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
const
|
|
877
|
-
const res =
|
|
878
|
-
if (!res.
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
const
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
const
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
try {
|
|
1022
|
-
const
|
|
1023
|
-
await this.
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
}
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
return reply(
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
|
-
if (cmd === '/
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
await this.
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
}
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
const
|
|
1112
|
-
|
|
1113
|
-
chatId
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
return reply(
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
const
|
|
1175
|
-
const
|
|
1176
|
-
const
|
|
1177
|
-
const
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
}
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
if (!
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
}
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
}
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
})
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
if (
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
const
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
if (
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
}
|
|
1618
|
-
const
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
if (
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
}
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
}
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
}
|
|
1712
|
-
}
|
|
1713
|
-
|
|
1714
|
-
async
|
|
1715
|
-
const
|
|
1716
|
-
|
|
1717
|
-
const
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
}
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import { randomUUID } from 'node:crypto'
|
|
3
|
+
import { join, dirname, resolve } from 'node:path'
|
|
4
|
+
import { homedir } from 'node:os'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
import { installModelSelection } from '@deepseek-ai/dsh-agent'
|
|
7
|
+
import { createUserMessage } from '@deepseek-ai/dsh-llm'
|
|
8
|
+
import { SessionId } from '@deepseek-ai/dsh-session'
|
|
9
|
+
import createAdapters from './adapters/index.js'
|
|
10
|
+
import { transcribeVoice, speakText } from './integrations.js'
|
|
11
|
+
import { attachInboundPhoto, photoOnlyHint } from './photos.js'
|
|
12
|
+
import { formatInboundDocument, documentOnlyHint, parseDocument } from './documents.js'
|
|
13
|
+
import { listFiles, getFileForDownload, formatFileSize } from './file-manager.js'
|
|
14
|
+
import { collectAssistantParts, buildOutboundFiles, stripImageUrls } from './outbound.js'
|
|
15
|
+
import { assistantText, splitText, stripReasoningPreamble, MESSENGER_RELAY_INSTRUCTION } from './text.js'
|
|
16
|
+
import { chatKey, sessionKey } from './topics.js'
|
|
17
|
+
import { listHomes, resolveNamedHome, upsertHome, normalizeHomeName } from './homes.js'
|
|
18
|
+
import { createVoicePrefs, shouldSpeakReply } from './voice-prefs.js'
|
|
19
|
+
import { prepareTtsText, toTelegramVoiceFile } from './tts.js'
|
|
20
|
+
import {
|
|
21
|
+
makeAskToken, buildInlineKeyboard, buildMultiSelectKeyboard, indexCallbacks, releaseCallbacks,
|
|
22
|
+
parseCallbackData, parseAskCallback, targetMatchesAsk, rejectPendingAsk, REMOVE_KEYBOARD,
|
|
23
|
+
} from './ask.js'
|
|
24
|
+
import { processDiagramsAndTables } from './artifacts.js'
|
|
25
|
+
import { createPersonaStore, getPersona, listPersonas, BUILTIN_PERSONAS } from './personas.js'
|
|
26
|
+
import { exportSessionToMarkdown, rewindSession } from './session-ops.js'
|
|
27
|
+
import { formatAlertMessage, resolveAlertTarget } from './alerts.js'
|
|
28
|
+
import { createScheduler, parseRelativeTime, formatRemaining } from './scheduler.js'
|
|
29
|
+
import { listModelCatalog, buildProvidersKeyboard, buildModelsKeyboard, getStoredModelSelection } from './models.js'
|
|
30
|
+
import { buildQuickActionsKeyboard, REMOVE_REPLY_KEYBOARD } from './adapters/telegram.js'
|
|
31
|
+
import { createPairingStore } from './pairing.js'
|
|
32
|
+
import {
|
|
33
|
+
extractTextDelta, extractToolName, buildStreamPreview, formatProgressLine,
|
|
34
|
+
createEditScheduler, startTypingHeartbeat,
|
|
35
|
+
} from './stream.js'
|
|
36
|
+
import { isTopicGoneError } from './telegram-errors.js'
|
|
37
|
+
import { ensureContentArray } from './content-guard.js'
|
|
38
|
+
import { mergeDynamicCommands } from './commands.js'
|
|
39
|
+
import { t } from './locales/index.js'
|
|
40
|
+
import { getUpdateStatus, runDirectUpdate } from './updater.js'
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
function whenIdleWithTimeout(agent, timeoutMs, signal) {
|
|
44
|
+
const idle = agent.whenIdle()
|
|
45
|
+
if (!timeoutMs || timeoutMs <= 0) return idle
|
|
46
|
+
return Promise.race([
|
|
47
|
+
idle,
|
|
48
|
+
new Promise((_, reject) => {
|
|
49
|
+
const timer = setTimeout(() => reject(new Error(`turn timeout (${timeoutMs}ms)`)), timeoutMs)
|
|
50
|
+
timer.unref?.()
|
|
51
|
+
signal?.addEventListener('abort', () => { clearTimeout(timer); reject(new DOMException('Aborted', 'AbortError')) }, { once: true })
|
|
52
|
+
}),
|
|
53
|
+
])
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function releaseChatTurn(chat) {
|
|
57
|
+
chat.busy = Promise.resolve()
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const PLUGIN = 'dsh-messenger-gateway'
|
|
61
|
+
|
|
62
|
+
export class Gateway {
|
|
63
|
+
constructor(ctx, config, hooks = {}) {
|
|
64
|
+
this.ctx = ctx
|
|
65
|
+
this.config = config
|
|
66
|
+
this.hooks = hooks
|
|
67
|
+
this.chats = new Map()
|
|
68
|
+
this.sessionToChat = new Map()
|
|
69
|
+
this.sessionToThread = new Map()
|
|
70
|
+
this.threadToSession = new Map()
|
|
71
|
+
this.pending = new Map()
|
|
72
|
+
this.pendingAsks = new Map()
|
|
73
|
+
this.adapters = new Map()
|
|
74
|
+
this.adapterList = []
|
|
75
|
+
this.disposeListener = undefined
|
|
76
|
+
this.idleTimer = undefined
|
|
77
|
+
this.callbackIndex = new Map()
|
|
78
|
+
const home = process.env.DSH_HOME || join(homedir(), '.dsh')
|
|
79
|
+
this.pairing = createPairingStore(join(home, 'messenger-gateway', 'pairing.json'))
|
|
80
|
+
this.voicePrefs = createVoicePrefs(join(home, 'messenger-gateway', 'voice-prefs.json'))
|
|
81
|
+
this.chatTts = createVoicePrefs(join(home, 'messenger-gateway', 'chat-tts.json'))
|
|
82
|
+
this.muted = createVoicePrefs(join(home, 'messenger-gateway', 'muted.json'))
|
|
83
|
+
this.personas = createPersonaStore(join(home, 'messenger-gateway', 'personas.json'))
|
|
84
|
+
this.chatLocales = createVoicePrefs(join(home, 'messenger-gateway', 'chat-locales.json'))
|
|
85
|
+
this.scheduler = createScheduler(join(home, 'messenger-gateway', 'scheduled.json'), async (task) => {
|
|
86
|
+
const target = {
|
|
87
|
+
platform: task.platform || 'telegram',
|
|
88
|
+
chatId: task.chatId,
|
|
89
|
+
threadId: task.threadId || 0,
|
|
90
|
+
}
|
|
91
|
+
const locale = this.resolveLocale({ chatId: task.chatId })
|
|
92
|
+
if (task.prompt || task.action === 'prompt') {
|
|
93
|
+
const promptText = task.prompt || task.text
|
|
94
|
+
try {
|
|
95
|
+
await this.dispatchAutonomousPrompt(target, promptText, locale)
|
|
96
|
+
} catch (err) {
|
|
97
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
98
|
+
this.ctx.logger?.warn?.(`dsh-messenger-gateway: cron prompt error: ${msg}`)
|
|
99
|
+
await this.sendToMessenger(target, {
|
|
100
|
+
text: `⚠️ <b>[Cron Error]</b>\n${msg}`,
|
|
101
|
+
}).catch(() => {})
|
|
102
|
+
}
|
|
103
|
+
} else {
|
|
104
|
+
const text = t('remind.prefix', { text: task.text }, locale)
|
|
105
|
+
await this.sendToMessenger(target, { text })
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
this.stats = { sent: 0, errors: 0, startedAt: Date.now() }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
resolveLocale(input) {
|
|
112
|
+
if (input?.locale) return input.locale
|
|
113
|
+
const chatId = input?.chatId
|
|
114
|
+
if (chatId && this.chatLocales?.get(chatId)) return this.chatLocales.get(chatId)
|
|
115
|
+
if (input?.languageCode) {
|
|
116
|
+
const code = String(input.languageCode).toLowerCase()
|
|
117
|
+
if (code.startsWith('zh')) return 'zh'
|
|
118
|
+
if (code.startsWith('en')) return 'en'
|
|
119
|
+
}
|
|
120
|
+
return this.config?.defaultLocale || 'en'
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async dispatchAutonomousPrompt(target, promptText, locale = 'en') {
|
|
124
|
+
const key = this.sessionKeyFor(target)
|
|
125
|
+
const reply = async (payload) => {
|
|
126
|
+
await this.sendToMessenger(target, typeof payload === 'string' ? { text: payload } : payload)
|
|
127
|
+
}
|
|
128
|
+
const input = {
|
|
129
|
+
platform: target.platform || 'telegram',
|
|
130
|
+
chatId: target.chatId,
|
|
131
|
+
threadId: target.threadId || 0,
|
|
132
|
+
text: promptText,
|
|
133
|
+
reply,
|
|
134
|
+
locale,
|
|
135
|
+
}
|
|
136
|
+
const chat = await this.getOrCreateChat(key, input)
|
|
137
|
+
const turnInput = { ...input, attachments: [], inboundWasVoice: false }
|
|
138
|
+
chat.turnActive = true
|
|
139
|
+
try { chat.abort?.abort?.() } catch {}
|
|
140
|
+
chat.abort = new AbortController()
|
|
141
|
+
const run = chat.busy.then(() => this.runTurn(chat, turnInput, chat.abort.signal))
|
|
142
|
+
chat.busy = run.catch(() => {})
|
|
143
|
+
run.catch((err) => {
|
|
144
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
145
|
+
this.ctx.logger?.warn?.(`dsh-messenger-gateway: cron turn: ${msg}`)
|
|
146
|
+
chat.turnActive = false
|
|
147
|
+
chat.abort = undefined
|
|
148
|
+
})
|
|
149
|
+
return run
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
isMuted(chatId) { return this.muted.get(chatId) === true }
|
|
153
|
+
setMuted(chatId, on) { return this.muted.set(chatId, on) }
|
|
154
|
+
|
|
155
|
+
baseUrl() {
|
|
156
|
+
const raw = String(this.config.internalBaseURL || '').trim()
|
|
157
|
+
return raw || 'http://127.0.0.1:3080'
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
tg() { return this.config.telegram || {} }
|
|
161
|
+
|
|
162
|
+
effectiveAllowedIds() {
|
|
163
|
+
const fromConfig = (this.tg().allowedUserIds || []).map(Number).filter(Number.isFinite)
|
|
164
|
+
const fromPairing = this.pairing.listApproved()
|
|
165
|
+
return [...new Set([...fromConfig, ...fromPairing])]
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
isUserAllowed(userId) {
|
|
169
|
+
const ids = this.effectiveAllowedIds()
|
|
170
|
+
return ids.length === 0 || ids.includes(Number(userId))
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
resolveHomeTarget(platform = 'telegram', name) {
|
|
174
|
+
const tg = this.tg()
|
|
175
|
+
const home = resolveNamedHome(tg, name)
|
|
176
|
+
if (!home) return null
|
|
177
|
+
const out = { platform, chatId: home.chatId, homeName: home.name }
|
|
178
|
+
if (home.threadId > 0) out.threadId = home.threadId
|
|
179
|
+
return out
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async sendAlert(type, payload = {}) {
|
|
183
|
+
try {
|
|
184
|
+
const target = resolveAlertTarget(this)
|
|
185
|
+
if (!target) return
|
|
186
|
+
const allowedEvents = this.config.telegram?.alerts?.events || ['error', 'pairing']
|
|
187
|
+
if (type !== 'status' && !allowedEvents.includes(type)) return
|
|
188
|
+
|
|
189
|
+
const text = formatAlertMessage(type, payload)
|
|
190
|
+
await this.sendToMessenger(target, { text })
|
|
191
|
+
} catch (err) {
|
|
192
|
+
this.ctx.logger?.warn?.(`sendAlert (${type}): ${err.message}`)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async start() {
|
|
197
|
+
this.disposeListener = this.ctx.on('session/event', (session, event) => {
|
|
198
|
+
if (this.config.telegram?.forumMirrorEnabled) {
|
|
199
|
+
if (event.type === 'turn/start' || event.type === 'session/create') {
|
|
200
|
+
this.mirrorSessionToForumTopic(session).catch?.(() => {})
|
|
201
|
+
} else if (event.type === 'turn/end') {
|
|
202
|
+
this.relayTurnToForumMirror(session, event).catch?.(() => {})
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const collector = this.pending.get(session.id)
|
|
207
|
+
if (!collector) return
|
|
208
|
+
if (event.type === 'assistant/message') {
|
|
209
|
+
const msg = event.data.message
|
|
210
|
+
const text = assistantText(msg)
|
|
211
|
+
if (text) collector.lastText = text
|
|
212
|
+
const extra = collectAssistantParts(msg)
|
|
213
|
+
for (const img of extra.images) collector.images.push(img)
|
|
214
|
+
} else if (event.type === 'assistant/chunk') {
|
|
215
|
+
const delta = extractTextDelta(event.data?.chunk)
|
|
216
|
+
if (delta) {
|
|
217
|
+
collector.streamText = (collector.streamText || '') + delta
|
|
218
|
+
collector.onStream?.(collector.streamText, collector.toolName)
|
|
219
|
+
}
|
|
220
|
+
} else if (event.type === 'tool/call') {
|
|
221
|
+
collector.toolName = extractToolName(event.data) || collector.toolName
|
|
222
|
+
collector.onStream?.(collector.streamText || '', collector.toolName)
|
|
223
|
+
} else if (event.type === 'tool/result') {
|
|
224
|
+
collector.toolName = ''
|
|
225
|
+
collector.onStream?.(collector.streamText || '', '')
|
|
226
|
+
} else if (event.type === 'turn/end') {
|
|
227
|
+
collector.reason = event.data.reason
|
|
228
|
+
}
|
|
229
|
+
})
|
|
230
|
+
const adapters = createAdapters({
|
|
231
|
+
config: this.config,
|
|
232
|
+
onMessage: (input) => this.handleMessage(input),
|
|
233
|
+
onCallback: (cb) => this.handleCallback(cb),
|
|
234
|
+
onUnauthorized: (input) => this.handleUnauthorized(input),
|
|
235
|
+
isUserAllowed: (id) => this.isUserAllowed(id),
|
|
236
|
+
logger: this.ctx.logger,
|
|
237
|
+
})
|
|
238
|
+
for (const adapter of adapters) {
|
|
239
|
+
try {
|
|
240
|
+
await adapter.start()
|
|
241
|
+
this.adapterList.push(adapter)
|
|
242
|
+
this.adapters.set(adapter.name, adapter)
|
|
243
|
+
if (adapter.name === 'telegram') this.tgAdapter = adapter
|
|
244
|
+
this.ctx.logger?.info?.(`dsh-messenger-gateway: ${adapter.name} started`)
|
|
245
|
+
} catch (err) {
|
|
246
|
+
this.ctx.logger?.warn?.(`dsh-messenger-gateway: ${adapter.name}: ${err.message}`)
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (this.adapters.has('telegram')) {
|
|
250
|
+
await this.syncTelegramCommands().catch((err) => {
|
|
251
|
+
this.ctx.logger?.warn?.(`Initial syncTelegramCommands: ${err?.message || err}`)
|
|
252
|
+
})
|
|
253
|
+
}
|
|
254
|
+
const rawIdle = Number(this.config.agent?.idleTimeoutMs)
|
|
255
|
+
const idleMs = Number.isFinite(rawIdle) && rawIdle > 0 ? rawIdle : 86_400_000
|
|
256
|
+
this.idleTimer = setInterval(() => this.reapIdle(), Math.min(idleMs, 60_000))
|
|
257
|
+
this.idleTimer.unref?.()
|
|
258
|
+
this.scheduler.start()
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
stop() {
|
|
262
|
+
if (this.scheduler) this.scheduler.stop()
|
|
263
|
+
if (this.disposeListener) this.disposeListener()
|
|
264
|
+
if (this.idleTimer) clearInterval(this.idleTimer)
|
|
265
|
+
for (const a of this.adapterList) { try { a.stop() } catch {} }
|
|
266
|
+
this.adapterList = []
|
|
267
|
+
this.adapters.clear()
|
|
268
|
+
for (const chat of this.chats.values()) chat.dispose().catch(() => {})
|
|
269
|
+
this.chats.clear()
|
|
270
|
+
this.sessionToChat.clear()
|
|
271
|
+
this.sessionToThread.clear()
|
|
272
|
+
this.threadToSession.clear()
|
|
273
|
+
for (const pending of this.pendingAsks.values()) {
|
|
274
|
+
releaseCallbacks(this.callbackIndex, pending.callbackKeys)
|
|
275
|
+
rejectPendingAsk(pending, new Error('gateway stopped'))
|
|
276
|
+
}
|
|
277
|
+
this.pending.clear()
|
|
278
|
+
this.pendingAsks.clear()
|
|
279
|
+
this.callbackIndex.clear()
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
getAdapter(platform) { return this.adapters.get(platform) }
|
|
283
|
+
|
|
284
|
+
async messengerSend(target, payload) {
|
|
285
|
+
let resolved = target
|
|
286
|
+
if (!resolved?.chatId && resolved?.platform) {
|
|
287
|
+
const home = this.resolveHomeTarget(resolved.platform, resolved.home || resolved.name)
|
|
288
|
+
if (!home) throw new Error('target.chatId required (or set telegram home channel)')
|
|
289
|
+
resolved = { ...home, ...resolved, chatId: home.chatId, threadId: resolved.threadId ?? home.threadId }
|
|
290
|
+
}
|
|
291
|
+
const adapter = this.getAdapter(resolved.platform)
|
|
292
|
+
if (!adapter?.sendTo) throw new Error(`adapter ${resolved.platform} unavailable`)
|
|
293
|
+
try {
|
|
294
|
+
await adapter.sendTo(resolved.chatId, payload, { threadId: resolved.threadId })
|
|
295
|
+
this.stats.sent++
|
|
296
|
+
} catch (err) {
|
|
297
|
+
this.stats.errors++
|
|
298
|
+
if (isTopicGoneError(err)) {
|
|
299
|
+
this.logger?.warn?.(`messengerSend: topic gone for ${resolved.platform}:${resolved.chatId}:${resolved.threadId} — the chat/topic was deleted; skipping delivery`)
|
|
300
|
+
return
|
|
301
|
+
}
|
|
302
|
+
throw err
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async messengerAsk(target, payload, timeoutMs = 300_000) {
|
|
307
|
+
const adapter = this.getAdapter(target.platform)
|
|
308
|
+
if (!adapter) throw new Error(`adapter ${target.platform} unavailable`)
|
|
309
|
+
const token = makeAskToken()
|
|
310
|
+
const isMulti = payload.mode === 'multi' || (Array.isArray(payload.options) && payload.options.length > 0)
|
|
311
|
+
let replyMarkup
|
|
312
|
+
let callbackKeys
|
|
313
|
+
let selectedSet
|
|
314
|
+
let page = 0
|
|
315
|
+
const pageSize = Number(payload.pageSize) || 6
|
|
316
|
+
|
|
317
|
+
if (isMulti) {
|
|
318
|
+
selectedSet = new Set(Array.isArray(payload.selected) ? payload.selected : [])
|
|
319
|
+
const kb = buildMultiSelectKeyboard(token, payload.options || payload.buttons, selectedSet, page, pageSize, payload)
|
|
320
|
+
replyMarkup = kb.replyMarkup
|
|
321
|
+
callbackKeys = kb.callbackKeys
|
|
322
|
+
} else {
|
|
323
|
+
const kb = buildInlineKeyboard(token, payload.buttons || [])
|
|
324
|
+
replyMarkup = kb.replyMarkup
|
|
325
|
+
callbackKeys = kb.callbackKeys
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
indexCallbacks(this.callbackIndex, callbackKeys, token)
|
|
329
|
+
try {
|
|
330
|
+
await adapter.sendTo(target.chatId, { text: payload.text, replyMarkup }, { threadId: target.threadId })
|
|
331
|
+
} catch (err) {
|
|
332
|
+
// Never leave stale callback keys pointing at an unresolvable ask.
|
|
333
|
+
releaseCallbacks(this.callbackIndex, callbackKeys)
|
|
334
|
+
if (isTopicGoneError(err)) {
|
|
335
|
+
this.logger?.warn?.(`messengerAsk: topic gone for ${target.platform}:${target.chatId}:${target.threadId} — ask aborted (chat/topic deleted)`)
|
|
336
|
+
throw new Error('messenger.ask: chat or topic was deleted')
|
|
337
|
+
}
|
|
338
|
+
throw err
|
|
339
|
+
}
|
|
340
|
+
return new Promise((resolve, reject) => {
|
|
341
|
+
const timer = setTimeout(() => {
|
|
342
|
+
this.pendingAsks.delete(token)
|
|
343
|
+
releaseCallbacks(this.callbackIndex, callbackKeys)
|
|
344
|
+
reject(new Error('messenger.ask timed out'))
|
|
345
|
+
}, timeoutMs)
|
|
346
|
+
timer.unref?.()
|
|
347
|
+
this.pendingAsks.set(token, {
|
|
348
|
+
resolve, reject, timer, target, callbackKeys, isMulti,
|
|
349
|
+
options: payload.options || payload.buttons,
|
|
350
|
+
selected: selectedSet,
|
|
351
|
+
page, pageSize, payload,
|
|
352
|
+
})
|
|
353
|
+
})
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
async messengerProgress(target, payload) {
|
|
357
|
+
await this.messengerSend(target, { text: payload.text })
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async handleUnauthorized(input) {
|
|
361
|
+
const { reply, userId, username } = input
|
|
362
|
+
const locale = this.resolveLocale(input)
|
|
363
|
+
if (!this.tg().pairingEnabled) {
|
|
364
|
+
await reply(t('msg.not_allowed', {}, locale))
|
|
365
|
+
return
|
|
366
|
+
}
|
|
367
|
+
try {
|
|
368
|
+
const { code } = this.pairing.requestCode(userId, { username })
|
|
369
|
+
await reply(t('msg.pairing_requested', { userId, code }, locale))
|
|
370
|
+
this.sendAlert('pairing', { userId, username, code }).catch(() => {})
|
|
371
|
+
} catch (err) {
|
|
372
|
+
if (err.code === 'RATE_LIMIT') await reply(t('msg.pairing_rate_limit', {}, locale))
|
|
373
|
+
else await reply(t('msg.exception', { message: err.message }, locale))
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
async handleCallback(cb) {
|
|
378
|
+
if (cb.userId && !this.isUserAllowed(cb.userId)) {
|
|
379
|
+
try { await cb.answer(t('msg.not_allowed', {}, 'en')) } catch {}
|
|
380
|
+
return
|
|
381
|
+
}
|
|
382
|
+
const indexed = this.callbackIndex.get(cb.data)
|
|
383
|
+
const { token, buttonId } = parseCallbackData(cb.data)
|
|
384
|
+
const askToken = indexed || token
|
|
385
|
+
if (askToken && this.pendingAsks.has(askToken)) {
|
|
386
|
+
const pending = this.pendingAsks.get(askToken)
|
|
387
|
+
if (!targetMatchesAsk(pending, cb)) {
|
|
388
|
+
await cb.answer(t('ask.other_chat', {}, 'en'))
|
|
389
|
+
return
|
|
390
|
+
}
|
|
391
|
+
const action = parseAskCallback(cb.data)
|
|
392
|
+
if (pending.isMulti) {
|
|
393
|
+
if (action.kind === 'toggle') {
|
|
394
|
+
if (pending.selected.has(action.id)) pending.selected.delete(action.id)
|
|
395
|
+
else pending.selected.add(action.id)
|
|
396
|
+
releaseCallbacks(this.callbackIndex, pending.callbackKeys)
|
|
397
|
+
const nextKb = buildMultiSelectKeyboard(askToken, pending.options, pending.selected, pending.page, pending.pageSize, pending.payload)
|
|
398
|
+
pending.callbackKeys = nextKb.callbackKeys
|
|
399
|
+
indexCallbacks(this.callbackIndex, nextKb.callbackKeys, askToken)
|
|
400
|
+
try {
|
|
401
|
+
if (cb.editReplyMarkup) await cb.editReplyMarkup(nextKb.replyMarkup)
|
|
402
|
+
else await cb.editMessage(cb.message?.text || 'Selection', nextKb.replyMarkup)
|
|
403
|
+
} catch {}
|
|
404
|
+
await cb.answer(pending.selected.has(action.id) ? 'Selected' : 'Deselected')
|
|
405
|
+
return
|
|
406
|
+
}
|
|
407
|
+
if (action.kind === 'page') {
|
|
408
|
+
pending.page = action.page
|
|
409
|
+
releaseCallbacks(this.callbackIndex, pending.callbackKeys)
|
|
410
|
+
const nextKb = buildMultiSelectKeyboard(askToken, pending.options, pending.selected, pending.page, pending.pageSize, pending.payload)
|
|
411
|
+
pending.callbackKeys = nextKb.callbackKeys
|
|
412
|
+
indexCallbacks(this.callbackIndex, nextKb.callbackKeys, askToken)
|
|
413
|
+
try {
|
|
414
|
+
if (cb.editReplyMarkup) await cb.editReplyMarkup(nextKb.replyMarkup)
|
|
415
|
+
else await cb.editMessage(cb.message?.text || 'Selection', nextKb.replyMarkup)
|
|
416
|
+
} catch {}
|
|
417
|
+
await cb.answer()
|
|
418
|
+
return
|
|
419
|
+
}
|
|
420
|
+
if (action.kind === 'done') {
|
|
421
|
+
this.pendingAsks.delete(askToken)
|
|
422
|
+
clearTimeout(pending.timer)
|
|
423
|
+
releaseCallbacks(this.callbackIndex, pending.callbackKeys)
|
|
424
|
+
await cb.answer('OK')
|
|
425
|
+
try { await cb.editMessage(cb.message?.text || 'Done', REMOVE_KEYBOARD) } catch {}
|
|
426
|
+
pending.resolve({ buttonId: 'done', selected: Array.from(pending.selected), data: cb.data })
|
|
427
|
+
return
|
|
428
|
+
}
|
|
429
|
+
if (action.kind === 'cancel') {
|
|
430
|
+
this.pendingAsks.delete(askToken)
|
|
431
|
+
clearTimeout(pending.timer)
|
|
432
|
+
releaseCallbacks(this.callbackIndex, pending.callbackKeys)
|
|
433
|
+
await cb.answer('Cancelled')
|
|
434
|
+
try { await cb.editMessage(cb.message?.text || 'Cancelled', REMOVE_KEYBOARD) } catch {}
|
|
435
|
+
pending.resolve({ buttonId: 'cancel', selected: [], data: cb.data })
|
|
436
|
+
return
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
this.pendingAsks.delete(askToken)
|
|
440
|
+
clearTimeout(pending.timer)
|
|
441
|
+
releaseCallbacks(this.callbackIndex, pending.callbackKeys)
|
|
442
|
+
await cb.answer('OK')
|
|
443
|
+
try { await cb.editMessage(cb.message?.text || 'Done', REMOVE_KEYBOARD) } catch {}
|
|
444
|
+
pending.resolve({ buttonId: action.id || buttonId, data: cb.data })
|
|
445
|
+
return
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// Model picker interactive flow
|
|
449
|
+
if (cb.data?.startsWith('mdl:')) {
|
|
450
|
+
const parts = cb.data.split(':')
|
|
451
|
+
const sub = parts[1]
|
|
452
|
+
// Step 2: Selected provider -> show its models (10 per page)
|
|
453
|
+
if (sub === 'p') {
|
|
454
|
+
const providerId = parts.slice(2).join(':')
|
|
455
|
+
const current = this.resolveAgentModel()
|
|
456
|
+
const catalog = await listModelCatalog(this.ctx, current)
|
|
457
|
+
const models = catalog.modelsByProvider.get(providerId) || []
|
|
458
|
+
if (!models.length) {
|
|
459
|
+
await cb.answer(t('model.no_models', {}, 'en'))
|
|
460
|
+
return
|
|
461
|
+
}
|
|
462
|
+
const kb = buildModelsKeyboard(providerId, models, current.model, 0)
|
|
463
|
+
await cb.answer()
|
|
464
|
+
const text = [
|
|
465
|
+
`🤖 <b>Provider:</b> <code>${providerId}</code>`,
|
|
466
|
+
`Select model (page ${kb.page + 1}/${kb.totalPages}):`,
|
|
467
|
+
].join('\n')
|
|
468
|
+
try {
|
|
469
|
+
if (cb.editMessage) await cb.editMessage(text, kb)
|
|
470
|
+
} catch {}
|
|
471
|
+
return
|
|
472
|
+
}
|
|
473
|
+
// Pagination for models
|
|
474
|
+
if (sub === 'pg') {
|
|
475
|
+
const page = parseInt(parts[parts.length - 1], 10) || 0
|
|
476
|
+
const providerId = parts.slice(2, -1).join(':')
|
|
477
|
+
const current = this.resolveAgentModel()
|
|
478
|
+
const catalog = await listModelCatalog(this.ctx, current)
|
|
479
|
+
const models = catalog.modelsByProvider.get(providerId) || []
|
|
480
|
+
const kb = buildModelsKeyboard(providerId, models, current.model, page)
|
|
481
|
+
await cb.answer()
|
|
482
|
+
const text = [
|
|
483
|
+
`🤖 <b>Provider:</b> <code>${providerId}</code>`,
|
|
484
|
+
`Select model (page ${kb.page + 1}/${kb.totalPages}):`,
|
|
485
|
+
].join('\n')
|
|
486
|
+
try {
|
|
487
|
+
if (cb.editMessage) await cb.editMessage(text, kb)
|
|
488
|
+
} catch {}
|
|
489
|
+
return
|
|
490
|
+
}
|
|
491
|
+
// Back to providers
|
|
492
|
+
if (sub === 'back') {
|
|
493
|
+
const current = this.resolveAgentModel()
|
|
494
|
+
const catalog = await listModelCatalog(this.ctx, current)
|
|
495
|
+
const kb = buildProvidersKeyboard(catalog.providers, current)
|
|
496
|
+
await cb.answer()
|
|
497
|
+
const text = [
|
|
498
|
+
'🤖 <b>Choose Provider:</b>',
|
|
499
|
+
`Current: <code>${current.provider}/${current.model}</code>`,
|
|
500
|
+
].join('\n')
|
|
501
|
+
try {
|
|
502
|
+
if (cb.editMessage) await cb.editMessage(text, kb)
|
|
503
|
+
} catch {}
|
|
504
|
+
return
|
|
505
|
+
}
|
|
506
|
+
// Select model
|
|
507
|
+
if (sub === 's') {
|
|
508
|
+
const key = parts[2]
|
|
509
|
+
const stored = getStoredModelSelection(key)
|
|
510
|
+
if (!stored) {
|
|
511
|
+
await cb.answer(t('ask.expired', {}, 'en'))
|
|
512
|
+
return
|
|
513
|
+
}
|
|
514
|
+
const { provider, model } = stored
|
|
515
|
+
try {
|
|
516
|
+
const adm = this.ctx.get('agentDefaultModel')
|
|
517
|
+
if (adm?.saveSelection) {
|
|
518
|
+
await adm.saveSelection({ provider, model })
|
|
519
|
+
}
|
|
520
|
+
this.config.agent = { ...this.config.agent, provider, model }
|
|
521
|
+
try {
|
|
522
|
+
await this.hooks?.persistAgentModel?.({ provider, model })
|
|
523
|
+
} catch (e) {
|
|
524
|
+
this.ctx.logger?.warn?.(`persist agent model: ${e.message}`)
|
|
525
|
+
}
|
|
526
|
+
await cb.answer(t('ask.chose', { choice: model }, 'en'))
|
|
527
|
+
try {
|
|
528
|
+
if (cb.editMessage) await cb.editMessage(t('model.switched', { provider, model }, 'en'), REMOVE_KEYBOARD)
|
|
529
|
+
} catch {}
|
|
530
|
+
} catch (err) {
|
|
531
|
+
await cb.answer(`Error: ${err.message}`)
|
|
532
|
+
}
|
|
533
|
+
return
|
|
534
|
+
}
|
|
535
|
+
if (sub === 'cur') {
|
|
536
|
+
await cb.answer()
|
|
537
|
+
return
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
await cb.answer()
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
sessionKeyFor(input) {
|
|
545
|
+
const scope = this.config.agent?.sessionScope || 'user'
|
|
546
|
+
return sessionKey({
|
|
547
|
+
platform: input.platform,
|
|
548
|
+
chatId: input.chatId,
|
|
549
|
+
threadId: input.threadId || 0,
|
|
550
|
+
userId: input.userId,
|
|
551
|
+
chatType: input.chatType,
|
|
552
|
+
scope,
|
|
553
|
+
})
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
isChatBusy(chat) {
|
|
557
|
+
return Boolean(chat?.turnActive)
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
async handleMessage(input) {
|
|
561
|
+
const { platform, chatId, threadId = 0, text, reply } = input
|
|
562
|
+
const key = this.sessionKeyFor(input)
|
|
563
|
+
const body = String(text || '').trim()
|
|
564
|
+
let attachments = [...(input.attachments || [])]
|
|
565
|
+
const hasMedia = attachments.length > 0
|
|
566
|
+
if (!body && !hasMedia) return
|
|
567
|
+
if (body.startsWith('/')) { await this.handleCommand(key, body, input); return }
|
|
568
|
+
try {
|
|
569
|
+
const chat = await this.getOrCreateChat(key, input)
|
|
570
|
+
const photoOnlyMode = this.config.agent?.photoOnlyMode ?? 'prompt'
|
|
571
|
+
const incomingPhotoOnly = hasMedia && attachments.every((a) => a.kind === 'photo' || a.kind === 'sticker') && !body
|
|
572
|
+
if (incomingPhotoOnly && photoOnlyMode === 'prompt') {
|
|
573
|
+
chat.pendingMedia = [...(chat.pendingMedia || []), ...attachments]
|
|
574
|
+
const n = chat.pendingMedia.length
|
|
575
|
+
const locale = this.resolveLocale(input)
|
|
576
|
+
const msg = n === 1
|
|
577
|
+
? t('photo.received_one', {}, locale)
|
|
578
|
+
: t('photo.received_many', { count: n }, locale)
|
|
579
|
+
return reply(msg)
|
|
580
|
+
}
|
|
581
|
+
if (chat.pendingMedia?.length) {
|
|
582
|
+
attachments = [...chat.pendingMedia, ...attachments]
|
|
583
|
+
chat.pendingMedia = []
|
|
584
|
+
}
|
|
585
|
+
const inboundWasVoice = attachments.some((a) => a.kind === 'voice' || a.kind === 'audio')
|
|
586
|
+
let personaOverride
|
|
587
|
+
for (const [pId] of Object.entries(BUILTIN_PERSONAS)) {
|
|
588
|
+
if (pId === 'default') continue
|
|
589
|
+
const tag = `@${pId}`
|
|
590
|
+
if (body.toLowerCase().includes(tag)) {
|
|
591
|
+
personaOverride = pId
|
|
592
|
+
break
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
const turnInput = { ...input, text: body, attachments, inboundWasVoice, personaOverride }
|
|
596
|
+
|
|
597
|
+
// Hermes-like steer: while a turn is running, inject followup instead of abort+restart
|
|
598
|
+
if (this.isChatBusy(chat)) {
|
|
599
|
+
const steerText = body || (hasMedia ? '(steer: media)' : '')
|
|
600
|
+
const content = await this.buildUserContent({
|
|
601
|
+
...turnInput,
|
|
602
|
+
text: steerText,
|
|
603
|
+
steer: true,
|
|
604
|
+
}, undefined)
|
|
605
|
+
chat.agent.followup(createUserMessage({
|
|
606
|
+
content: ensureContentArray(content),
|
|
607
|
+
source: { kind: 'user', plugin: PLUGIN, form: 'steer', origin: 'telegram' },
|
|
608
|
+
}))
|
|
609
|
+
chat.lastUsed = Date.now()
|
|
610
|
+
try { await reply(t('msg.steer_added', {}, this.resolveLocale(input))) } catch {}
|
|
611
|
+
return
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Mark busy BEFORE yielding to the poll loop, otherwise steer/stop never see an active turn.
|
|
615
|
+
chat.turnActive = true
|
|
616
|
+
try { chat.abort?.abort?.() } catch {}
|
|
617
|
+
chat.abort = new AbortController()
|
|
618
|
+
const run = chat.busy.then(() => this.runTurn(chat, turnInput, chat.abort.signal))
|
|
619
|
+
chat.busy = run.catch(() => {})
|
|
620
|
+
// Do NOT await: Telegram poll is sequential; awaiting blocked steer and /stop until the turn finished.
|
|
621
|
+
run.catch((err) => {
|
|
622
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
623
|
+
this.ctx.logger?.warn?.(`dsh-messenger-gateway: background turn: ${msg}`)
|
|
624
|
+
this.sendAlert('error', {
|
|
625
|
+
code: err?.code || 'BACKGROUND_ERROR',
|
|
626
|
+
message: msg,
|
|
627
|
+
sessionId: chat.agent?.session?.id,
|
|
628
|
+
chatId: input.chatId,
|
|
629
|
+
threadId: input.threadId,
|
|
630
|
+
}).catch(() => {})
|
|
631
|
+
chat.turnActive = false
|
|
632
|
+
chat.abort = undefined
|
|
633
|
+
})
|
|
634
|
+
} catch (err) {
|
|
635
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
636
|
+
this.ctx.logger?.warn?.(`dsh-messenger-gateway: message: ${msg}`)
|
|
637
|
+
try { await reply(t('msg.exception', { message: msg }, this.resolveLocale(input))) } catch {}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
async handleCommand(key, text, input) {
|
|
642
|
+
const parts = text.split(/\s+/)
|
|
643
|
+
const cmd = parts[0].toLowerCase().split('@')[0]
|
|
644
|
+
const { reply, userId, chatId, threadId = 0, platform } = input
|
|
645
|
+
const locale = this.resolveLocale(input)
|
|
646
|
+
|
|
647
|
+
if (cmd === '/start') {
|
|
648
|
+
return reply(t('msg.start', {}, locale), { replyMarkup: REMOVE_REPLY_KEYBOARD })
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
if (cmd === '/help') {
|
|
652
|
+
return reply([
|
|
653
|
+
'📖 <b>Messenger Gateway Help:</b>',
|
|
654
|
+
'',
|
|
655
|
+
'💬 <b>Session & Chat:</b>',
|
|
656
|
+
'• /help — show this command reference',
|
|
657
|
+
'• /new — start fresh session',
|
|
658
|
+
'• /stop — interrupt current response',
|
|
659
|
+
'• /model — interactive model selector (/model list)',
|
|
660
|
+
'• /role [name] — switch persona or role (/role list)',
|
|
661
|
+
'• /bind [role] — bind persona to topic / chat',
|
|
662
|
+
'• /preset [name] — bind preset to topic / chat',
|
|
663
|
+
'• /lang [en|zh] — switch user language',
|
|
664
|
+
'• /rewind [N] — rewind last N turns',
|
|
665
|
+
'• /fork — fork session into new branch',
|
|
666
|
+
'• /export — export history to Markdown',
|
|
667
|
+
'',
|
|
668
|
+
'🛠️ <b>Tools, Files & Cron:</b>',
|
|
669
|
+
'• /skills / /tools — list active tools & skills',
|
|
670
|
+
'• /files [dir] — workspace file explorer',
|
|
671
|
+
'• /get <path> — download file from workspace',
|
|
672
|
+
'• /remind <time> <text> — set reminder (/remind 10m check deploy)',
|
|
673
|
+
'• /cron <interval> <prompt> — recurring autonomous task',
|
|
674
|
+
'',
|
|
675
|
+
'⚙️ <b>Settings & Stats:</b>',
|
|
676
|
+
'• /status — gateway and active model status',
|
|
677
|
+
'• /top — system resource usage (RAM, uptime)',
|
|
678
|
+
'• /keyboard on|off — quick action keyboard',
|
|
679
|
+
'• /voice on|off|status — voice replies preference',
|
|
680
|
+
'• /tts on|off|status — speech synthesis in this chat',
|
|
681
|
+
'• /mute / /unmute — mute notifications in this chat',
|
|
682
|
+
'',
|
|
683
|
+
'🔒 <b>Access & Channels:</b>',
|
|
684
|
+
'• /whoami — your messenger user ID',
|
|
685
|
+
'• /pair CODE — approve pairing code',
|
|
686
|
+
'• /sethome [name] — set home notification channel',
|
|
687
|
+
'• /setalert — set alert channel',
|
|
688
|
+
].join('\n'))
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
if (cmd === '/lang' || cmd === '/language') {
|
|
692
|
+
const sub = parts[1]?.toLowerCase()
|
|
693
|
+
if (sub === 'en' || sub === 'zh') {
|
|
694
|
+
this.chatLocales.set(chatId, sub)
|
|
695
|
+
return reply(sub === 'zh' ? '语言已切换为中文 (zh)' : 'Language switched to English (en)')
|
|
696
|
+
}
|
|
697
|
+
const cur = this.chatLocales.get(chatId) || this.config?.defaultLocale || 'en'
|
|
698
|
+
return reply(`Current language: <b>${cur}</b>\nSwitch: <code>/lang en</code> or <code>/lang zh</code>`)
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
if (cmd === '/bind') {
|
|
702
|
+
const targetRole = parts[1]?.toLowerCase()
|
|
703
|
+
if (!targetRole || targetRole === 'list') {
|
|
704
|
+
const cur = this.personas.getPersonaForChat(chatId, threadId)
|
|
705
|
+
return reply(`${t('persona.title', {}, locale)}\nCurrent bound role: <b>${cur}</b>\nUsage: <code>/bind <role></code> (or /bind reset)`)
|
|
706
|
+
}
|
|
707
|
+
if (targetRole === 'reset' || targetRole === 'default') {
|
|
708
|
+
this.personas.set(chatId, 'default', threadId)
|
|
709
|
+
return reply(t('persona.reset', {}, locale))
|
|
710
|
+
}
|
|
711
|
+
const persona = getPersona(targetRole)
|
|
712
|
+
if (!persona) return reply(t('persona.unknown', { target: targetRole }, locale))
|
|
713
|
+
this.personas.set(chatId, targetRole, threadId)
|
|
714
|
+
return reply(t('persona.bound_topic', { kind: 'role', name: `${persona.icon} ${persona.name}` }, locale))
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
if (cmd === '/preset') {
|
|
718
|
+
const presetName = parts[1]
|
|
719
|
+
if (!presetName || presetName === 'list') {
|
|
720
|
+
const cur = this.personas.getPreset(chatId, threadId) || '(none)'
|
|
721
|
+
return reply(`🎭 <b>Presets:</b>\nCurrent topic preset: <code>${cur}</code>\nUsage: <code>/preset <name></code> or <code>/preset reset</code>`)
|
|
722
|
+
}
|
|
723
|
+
if (presetName === 'reset' || presetName === 'clear') {
|
|
724
|
+
this.personas.setPreset(chatId, threadId, null)
|
|
725
|
+
return reply('Preset cleared for this topic.')
|
|
726
|
+
}
|
|
727
|
+
this.personas.setPreset(chatId, threadId, presetName)
|
|
728
|
+
return reply(t('persona.bound_topic', { kind: 'preset', name: presetName }, locale))
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
if (cmd === '/cron') {
|
|
732
|
+
const sub = parts[1]?.toLowerCase()
|
|
733
|
+
if (sub === 'list') {
|
|
734
|
+
const list = await this.scheduler.listRecurring(chatId)
|
|
735
|
+
if (!list.length) return reply(t('cron.none', {}, locale))
|
|
736
|
+
const lines = [t('cron.list_title', {}, locale)]
|
|
737
|
+
for (const task of list) {
|
|
738
|
+
const left = formatRemaining(task.dueAt - Date.now(), locale)
|
|
739
|
+
lines.push(`• <code>${task.id}</code> (every ${formatRemaining(task.intervalMs, locale)}, next in ${left}): ${task.prompt || task.text}`)
|
|
740
|
+
}
|
|
741
|
+
lines.push('\nCancel: <code>/cron cancel ID</code>')
|
|
742
|
+
return reply(lines.join('\n'))
|
|
743
|
+
}
|
|
744
|
+
if (sub === 'cancel') {
|
|
745
|
+
const targetId = parts[2]
|
|
746
|
+
if (!targetId) return reply('Specify cron task ID: <code>/cron cancel ID</code>')
|
|
747
|
+
const ok = await this.scheduler.cancel(targetId, chatId)
|
|
748
|
+
return reply(ok ? t('cron.cancelled', { id: targetId }, locale) : `Task not found: <code>${targetId}</code>`)
|
|
749
|
+
}
|
|
750
|
+
const specArg = parts[1]
|
|
751
|
+
const promptArg = parts.slice(2).join(' ')
|
|
752
|
+
const ms = parseRelativeTime(specArg)
|
|
753
|
+
if (!ms || !promptArg) {
|
|
754
|
+
return reply('⏱️ <b>Autonomous Cron Tasks:</b>\nCreate: <code>/cron <interval> <prompt></code>\nExample: <code>/cron 1h check server logs</code>\nList: <code>/cron list</code>\nCancel: <code>/cron cancel ID</code>')
|
|
755
|
+
}
|
|
756
|
+
const task = await this.scheduler.schedule({
|
|
757
|
+
platform,
|
|
758
|
+
chatId,
|
|
759
|
+
threadId,
|
|
760
|
+
userId,
|
|
761
|
+
text: promptArg,
|
|
762
|
+
prompt: promptArg,
|
|
763
|
+
dueAt: Date.now() + ms,
|
|
764
|
+
recurring: true,
|
|
765
|
+
intervalMs: ms,
|
|
766
|
+
})
|
|
767
|
+
return reply(t('cron.scheduled', { id: task.id, schedule: specArg, prompt: promptArg }, locale))
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
if (cmd === '/update') {
|
|
771
|
+
if (!this.isUserAllowed(userId)) {
|
|
772
|
+
return reply(t('msg.not_allowed', {}, locale))
|
|
773
|
+
}
|
|
774
|
+
const sub = parts[1]?.toLowerCase()
|
|
775
|
+
const manifestPath = resolve(dirname(fileURLToPath(import.meta.url)), '../package.json')
|
|
776
|
+
const updaterOpts = {
|
|
777
|
+
manifestPath,
|
|
778
|
+
packageName: '@goodandready/dsh-messenger-gateway',
|
|
779
|
+
}
|
|
780
|
+
if (sub === 'now' || sub === 'install') {
|
|
781
|
+
await reply(t('update.installing', {}, locale))
|
|
782
|
+
try {
|
|
783
|
+
const res = await runDirectUpdate(updaterOpts)
|
|
784
|
+
if (!res.updated) {
|
|
785
|
+
return reply(t('update.already_latest', { version: res.currentVersion }, locale))
|
|
786
|
+
}
|
|
787
|
+
return reply(t('update.success', { version: res.updatedVersion }, locale))
|
|
788
|
+
} catch (err) {
|
|
789
|
+
return reply(t('update.failed', { error: err.message }, locale))
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
try {
|
|
793
|
+
const st = await getUpdateStatus(updaterOpts)
|
|
794
|
+
if (st.updateAvailable) {
|
|
795
|
+
return reply(t('update.available', { current: st.currentVersion, latest: st.latestVersion }, locale))
|
|
796
|
+
}
|
|
797
|
+
return reply(t('update.current', { version: st.currentVersion }, locale))
|
|
798
|
+
} catch (err) {
|
|
799
|
+
return reply(t('update.check_failed', { error: err.message }, locale))
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
if (cmd === '/role' || cmd === '/persona') {
|
|
804
|
+
const targetRole = parts[1]?.toLowerCase()
|
|
805
|
+
if (!targetRole || targetRole === 'list') {
|
|
806
|
+
const currentId = this.personas.getPersonaForChat(chatId, threadId)
|
|
807
|
+
const lines = [
|
|
808
|
+
t('persona.title', {}, locale),
|
|
809
|
+
'',
|
|
810
|
+
...listPersonas().map((p) => {
|
|
811
|
+
const isCurrent = p.id === currentId ? ' (active)' : ''
|
|
812
|
+
return `${p.icon} <b>${p.id}</b> — ${p.name}: ${p.description}${isCurrent}`
|
|
813
|
+
}),
|
|
814
|
+
'',
|
|
815
|
+
t('persona.usage', {}, locale),
|
|
816
|
+
]
|
|
817
|
+
return reply(lines.join('\n'))
|
|
818
|
+
}
|
|
819
|
+
if (targetRole === 'reset' || targetRole === 'default') {
|
|
820
|
+
this.personas.set(chatId, 'default', threadId)
|
|
821
|
+
return reply(t('persona.reset', {}, locale))
|
|
822
|
+
}
|
|
823
|
+
const persona = getPersona(targetRole)
|
|
824
|
+
if (!persona) {
|
|
825
|
+
return reply(t('persona.unknown', { target: targetRole }, locale))
|
|
826
|
+
}
|
|
827
|
+
this.personas.set(chatId, persona.id, threadId)
|
|
828
|
+
return reply(t('persona.switched', { icon: persona.icon, name: persona.name, description: persona.description }, locale))
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
if (cmd === '/skills' || cmd === '/tools') {
|
|
832
|
+
const tools = this.ctx.get?.('tools') || this.ctx.tools
|
|
833
|
+
const toolsList = []
|
|
834
|
+
if (tools?.tools) {
|
|
835
|
+
for (const [name, tDef] of tools.tools.entries()) {
|
|
836
|
+
toolsList.push(`• <b>${name}</b>: ${tDef.description || '(no description)'}`)
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
if (!toolsList.length) {
|
|
840
|
+
return reply('🛠️ <b>Agent Tools:</b>\n(no tools registered)')
|
|
841
|
+
}
|
|
842
|
+
return reply([
|
|
843
|
+
'🛠️ <b>Active Tools & Skills:</b>',
|
|
844
|
+
'',
|
|
845
|
+
...toolsList,
|
|
846
|
+
].join('\n'))
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
if (cmd === '/export') {
|
|
850
|
+
const chat = this.chats.get(key)
|
|
851
|
+
if (!chat?.agent?.session) {
|
|
852
|
+
return reply(t('msg.no_active_session', {}, locale))
|
|
853
|
+
}
|
|
854
|
+
try {
|
|
855
|
+
const { filename, buffer, messagesCount } = exportSessionToMarkdown(chat.agent.session)
|
|
856
|
+
if (!messagesCount) {
|
|
857
|
+
return reply(t('export.empty', {}, locale))
|
|
858
|
+
}
|
|
859
|
+
const file = {
|
|
860
|
+
name: filename,
|
|
861
|
+
mime: 'text/markdown',
|
|
862
|
+
kind: 'document',
|
|
863
|
+
bytes: buffer,
|
|
864
|
+
}
|
|
865
|
+
return reply({ text: t('export.title', { count: messagesCount }, locale), files: [file] })
|
|
866
|
+
} catch (err) {
|
|
867
|
+
return reply(`Export error: ${err.message}`)
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
if (cmd === '/rewind') {
|
|
872
|
+
const chat = this.chats.get(key)
|
|
873
|
+
if (!chat?.agent?.session) {
|
|
874
|
+
return reply(t('msg.no_active_session', {}, locale))
|
|
875
|
+
}
|
|
876
|
+
const count = Number(parts[1]) || 1
|
|
877
|
+
const res = rewindSession(chat.agent.session, count)
|
|
878
|
+
if (!res.removed) {
|
|
879
|
+
return reply('No turns to rewind in session history.')
|
|
880
|
+
}
|
|
881
|
+
const sessions = this.ctx.get?.('sessions') || this.ctx.sessions
|
|
882
|
+
try { await sessions?.flush(chat.agent.session) } catch {}
|
|
883
|
+
return reply(`⏪ Rewound ${res.removed} messages. Remaining in context: ${res.remaining}.`)
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
if (cmd === '/fork') {
|
|
887
|
+
const chat = this.chats.get(key)
|
|
888
|
+
if (!chat?.agent?.session) {
|
|
889
|
+
return reply(t('msg.no_active_session', {}, locale))
|
|
890
|
+
}
|
|
891
|
+
try {
|
|
892
|
+
const oldSession = chat.agent.session
|
|
893
|
+
const oldMessages = Array.isArray(oldSession.messages)
|
|
894
|
+
? oldSession.messages.map(m => ({ ...m, content: ensureContentArray(m.content) }))
|
|
895
|
+
: []
|
|
896
|
+
const newChat = await this.createChat(key, input)
|
|
897
|
+
if (newChat.agent?.session && oldMessages.length) {
|
|
898
|
+
newChat.agent.session.messages = oldMessages
|
|
899
|
+
const sessions = this.ctx.get?.('sessions') || this.ctx.sessions
|
|
900
|
+
try { await sessions?.flush(newChat.agent.session) } catch {}
|
|
901
|
+
}
|
|
902
|
+
this.chats.set(key, newChat)
|
|
903
|
+
return reply(`🔀 Forked session!\nOld session: ${oldSession.id}\nNew session: ${newChat.agent.session.id}\nContext preserved (${oldMessages.length} messages).`)
|
|
904
|
+
} catch (err) {
|
|
905
|
+
return reply(`Fork error: ${err.message}`)
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
if (cmd === '/files') {
|
|
910
|
+
const subPath = parts.slice(1).join(' ').trim() || '.'
|
|
911
|
+
const agentCwd = this.config.agent?.cwd || process.cwd()
|
|
912
|
+
const res = await listFiles(agentCwd, subPath)
|
|
913
|
+
if (!res.ok) return reply(`❌ ${res.error}`)
|
|
914
|
+
return reply(res.formattedText)
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
if (cmd === '/get') {
|
|
918
|
+
const targetRel = parts.slice(1).join(' ').trim()
|
|
919
|
+
if (!targetRel) {
|
|
920
|
+
return reply('Specify file path to download: <code>/get <path></code>\nBrowse: <code>/files</code>')
|
|
921
|
+
}
|
|
922
|
+
const agentCwd = this.config.agent?.cwd || process.cwd()
|
|
923
|
+
const maxDocBytes = Number(this.config.media?.maxDocBytes) || 50 * 1024 * 1024
|
|
924
|
+
const res = await getFileForDownload(agentCwd, targetRel, maxDocBytes)
|
|
925
|
+
if (!res.ok) return reply(`❌ ${res.error}`)
|
|
926
|
+
const file = {
|
|
927
|
+
name: res.name,
|
|
928
|
+
mime: res.mime,
|
|
929
|
+
kind: 'document',
|
|
930
|
+
bytes: res.bytes,
|
|
931
|
+
dataBase64: res.bytes.toString('base64'),
|
|
932
|
+
}
|
|
933
|
+
return reply({ text: `📄 File: <b>${res.name}</b> (${formatFileSize(res.size)})`, files: [file] })
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
if (cmd === '/new') {
|
|
937
|
+
const chat = this.chats.get(key)
|
|
938
|
+
if (chat) {
|
|
939
|
+
if (chat.abort) chat.abort.abort()
|
|
940
|
+
chat.pendingMedia = []
|
|
941
|
+
this.sessionToChat.delete(String(chat.agent.session.id))
|
|
942
|
+
this.chats.delete(key)
|
|
943
|
+
await chat.dispose()
|
|
944
|
+
return reply('Session reset.')
|
|
945
|
+
}
|
|
946
|
+
return reply(t('msg.no_active_session', {}, locale))
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
if (cmd === '/whoami') {
|
|
950
|
+
const lines = [`User ID: ${userId}`]
|
|
951
|
+
if (chatId) lines.push(`chatId: ${chatId}`)
|
|
952
|
+
if (threadId) lines.push(`threadId: ${threadId}`)
|
|
953
|
+
return reply(lines.join('\n'))
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
if (cmd === '/stop') {
|
|
957
|
+
const chat = this.chats.get(key)
|
|
958
|
+
if (chat?.turnActive || chat?.abort) {
|
|
959
|
+
try { chat.abort?.abort() } catch {}
|
|
960
|
+
releaseChatTurn(chat)
|
|
961
|
+
chat.turnActive = false
|
|
962
|
+
return reply(t('msg.turn_stopped', {}, locale))
|
|
963
|
+
}
|
|
964
|
+
return reply('Nothing to stop.')
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
if (cmd === '/status') {
|
|
968
|
+
let modelLine = 'model: (not set)'
|
|
969
|
+
try {
|
|
970
|
+
const sel = this.resolveAgentModel()
|
|
971
|
+
modelLine = `model: ${sel.provider}/${sel.model}`
|
|
972
|
+
} catch (e) {
|
|
973
|
+
modelLine = `model: ${e.message}`
|
|
974
|
+
}
|
|
975
|
+
const home = this.resolveHomeTarget(platform || 'telegram')
|
|
976
|
+
const homeLine = home
|
|
977
|
+
? `home: chat ${home.chatId}${home.threadId ? ` topic ${home.threadId}` : ''}`
|
|
978
|
+
: 'home: (not set)'
|
|
979
|
+
const pending = this.pairing.listPending().length
|
|
980
|
+
const up = Math.max(0, Math.round((Date.now() - this.stats.startedAt) / 1000))
|
|
981
|
+
const hh = String(Math.floor(up / 3600)).padStart(2, '0')
|
|
982
|
+
const mm = String(Math.floor((up % 3600) / 60)).padStart(2, '0')
|
|
983
|
+
const ss = String(up % 60).padStart(2, '0')
|
|
984
|
+
return reply([
|
|
985
|
+
'Messenger gateway',
|
|
986
|
+
`adapters: ${[...this.adapters.keys()].join(', ') || '(none)'}`,
|
|
987
|
+
`active chats: ${this.chats.size}`,
|
|
988
|
+
modelLine,
|
|
989
|
+
homeLine,
|
|
990
|
+
`pairing pending: ${pending}`,
|
|
991
|
+
`transport: ${this.tg().transport || 'poll'}`,
|
|
992
|
+
`sessionScope: ${this.config.agent?.sessionScope || 'user'}`,
|
|
993
|
+
`delivered: ${this.stats.sent}`,
|
|
994
|
+
`errors: ${this.stats.errors}`,
|
|
995
|
+
`polling conflict: ${this.tgAdapter?.pollingConflict ? 'yes' : 'no'}`,
|
|
996
|
+
`uptime: ${hh}:${mm}:${ss}`,
|
|
997
|
+
].join('\n'))
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
if (cmd === '/model') {
|
|
1001
|
+
if (parts.length >= 3) {
|
|
1002
|
+
if (!this.isUserAllowed(userId)) return reply(t('msg.not_allowed', {}, locale))
|
|
1003
|
+
const provider = parts[1]
|
|
1004
|
+
const model = parts.slice(2).join(' ')
|
|
1005
|
+
try {
|
|
1006
|
+
const adm = this.ctx.get('agentDefaultModel')
|
|
1007
|
+
if (adm?.saveSelection) {
|
|
1008
|
+
await adm.saveSelection({ provider, model })
|
|
1009
|
+
}
|
|
1010
|
+
this.config.agent = { ...this.config.agent, provider, model }
|
|
1011
|
+
try {
|
|
1012
|
+
await this.hooks?.persistAgentModel?.({ provider, model })
|
|
1013
|
+
} catch (e) {
|
|
1014
|
+
this.ctx.logger?.warn?.(`persist agent model: ${e.message}`)
|
|
1015
|
+
}
|
|
1016
|
+
return reply(t('model.switched', { provider, model }, locale))
|
|
1017
|
+
} catch (e) {
|
|
1018
|
+
return reply(`Failed to switch model: ${e.message}`)
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
try {
|
|
1022
|
+
const current = this.resolveAgentModel()
|
|
1023
|
+
const catalog = await listModelCatalog(this.ctx, current)
|
|
1024
|
+
if (!catalog.providers.length) {
|
|
1025
|
+
return reply(`Current model: <code>${current.provider}/${current.model}</code>\nSwitch: <code>/model <provider> <model></code>`)
|
|
1026
|
+
}
|
|
1027
|
+
const kb = buildProvidersKeyboard(catalog.providers, current)
|
|
1028
|
+
return reply([
|
|
1029
|
+
t('model.title', {}, locale),
|
|
1030
|
+
t('model.current', { current: `${current.provider}/${current.model}` }, locale),
|
|
1031
|
+
].join('\n'), {
|
|
1032
|
+
replyMarkup: kb,
|
|
1033
|
+
})
|
|
1034
|
+
} catch (e) {
|
|
1035
|
+
return reply(e.message)
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
if (cmd === '/pair') {
|
|
1040
|
+
if (!this.isUserAllowed(userId)) return reply('Only users in allowlist can approve /pair.')
|
|
1041
|
+
const code = parts[1]
|
|
1042
|
+
if (!code) return reply('Usage: /pair CODE')
|
|
1043
|
+
const res = this.pairing.approveCode(code, userId)
|
|
1044
|
+
if (!res.ok) return reply(`Failed: ${res.error}`)
|
|
1045
|
+
const merged = this.effectiveAllowedIds()
|
|
1046
|
+
for (const a of this.adapterList) a.setAllowedUserIds?.(merged)
|
|
1047
|
+
try { await this.hooks?.persistAllowedUserIds?.(merged) } catch (e) {
|
|
1048
|
+
this.ctx.logger?.warn?.(`persist allowlist: ${e.message}`)
|
|
1049
|
+
}
|
|
1050
|
+
return reply(`Approved user ID ${res.userId}${res.username ? ` (@${res.username})` : ''}.`)
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
if (cmd === '/sethome') {
|
|
1054
|
+
if (!this.isUserAllowed(userId)) return reply(t('msg.not_allowed', {}, locale))
|
|
1055
|
+
const name = normalizeHomeName(parts[1] || 'default') || 'default'
|
|
1056
|
+
try {
|
|
1057
|
+
const nextTg = upsertHome(this.tg(), { name, chatId, threadId })
|
|
1058
|
+
await this.hooks?.persistHomes?.(nextTg)
|
|
1059
|
+
this.config.telegram = nextTg
|
|
1060
|
+
return reply(`Home "${name}": chat ${chatId}${threadId ? ` topic ${threadId}` : ''}`)
|
|
1061
|
+
} catch (e) {
|
|
1062
|
+
return reply(`Failed to save home: ${e.message}`)
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
if (cmd === '/home') {
|
|
1067
|
+
const homes = listHomes(this.tg())
|
|
1068
|
+
if (!homes.length) return reply('Home is not set. /sethome or /sethome <name>')
|
|
1069
|
+
return reply(['Homes:', ...homes.map((h) => `• ${h.name}: chat ${h.chatId}${h.threadId ? ` topic ${h.threadId}` : ''}`)].join('\n'))
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
if (cmd === '/setalert') {
|
|
1073
|
+
if (!this.isUserAllowed(userId)) return reply(t('msg.not_allowed', {}, locale))
|
|
1074
|
+
const nextTg = {
|
|
1075
|
+
...this.tg(),
|
|
1076
|
+
alerts: {
|
|
1077
|
+
...(this.tg().alerts || {}),
|
|
1078
|
+
enabled: true,
|
|
1079
|
+
chatId,
|
|
1080
|
+
threadId: threadId || 0,
|
|
1081
|
+
},
|
|
1082
|
+
}
|
|
1083
|
+
this.config.telegram = nextTg
|
|
1084
|
+
try { await this.hooks?.persistHomes?.(nextTg) } catch {}
|
|
1085
|
+
return reply(`🔔 This chat assigned as alert channel (chat: ${chatId}${threadId ? `, topic: ${threadId}` : ''}).`)
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
if (cmd === '/alert') {
|
|
1089
|
+
const sub = parts[1]?.toLowerCase()
|
|
1090
|
+
if (sub === 'test') {
|
|
1091
|
+
const target = resolveAlertTarget(this)
|
|
1092
|
+
if (!target) return reply('Alert channel not configured. Configure: /setalert')
|
|
1093
|
+
await this.sendAlert('status', { title: 'Test Alert', details: `Sent by user ID ${userId}` })
|
|
1094
|
+
return reply('Test alert sent to alert channel.')
|
|
1095
|
+
}
|
|
1096
|
+
const target = resolveAlertTarget(this)
|
|
1097
|
+
const alertsCfg = this.tg().alerts || {}
|
|
1098
|
+
return reply([
|
|
1099
|
+
'🔔 <b>Alert Channel:</b>',
|
|
1100
|
+
`Status: ${alertsCfg.enabled ? 'enabled' : 'disabled'}`,
|
|
1101
|
+
`Chat: ${target ? `${target.chatId}${target.threadId ? ` (topic: ${target.threadId})` : ''}` : '(not assigned)'}`,
|
|
1102
|
+
`Events: ${(alertsCfg.events || ['error', 'pairing']).join(', ')}`,
|
|
1103
|
+
'',
|
|
1104
|
+
'Commands:',
|
|
1105
|
+
'/setalert — assign current chat as alert channel',
|
|
1106
|
+
'/alert test — send test alert',
|
|
1107
|
+
].join('\n'))
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
if (cmd === '/remind') {
|
|
1111
|
+
const sub = parts[1]?.toLowerCase()
|
|
1112
|
+
if (sub === 'list') {
|
|
1113
|
+
const active = await this.scheduler.list(chatId)
|
|
1114
|
+
if (!active.length) return reply('No active reminders for this chat.')
|
|
1115
|
+
const lines = [
|
|
1116
|
+
'⏰ <b>Active Reminders:</b>',
|
|
1117
|
+
'',
|
|
1118
|
+
...active.map((tItem) => {
|
|
1119
|
+
const left = formatRemaining(tItem.dueAt - Date.now(), locale)
|
|
1120
|
+
return `• <code>${tItem.id}</code> (in ${left}): ${tItem.text}`
|
|
1121
|
+
}),
|
|
1122
|
+
'',
|
|
1123
|
+
'Cancel: <code>/remind cancel ID</code>',
|
|
1124
|
+
]
|
|
1125
|
+
return reply(lines.join('\n'))
|
|
1126
|
+
}
|
|
1127
|
+
if (sub === 'cancel') {
|
|
1128
|
+
const targetId = parts[2]?.trim()
|
|
1129
|
+
if (!targetId) return reply('Specify reminder ID: <code>/remind cancel ID</code>')
|
|
1130
|
+
const ok = await this.scheduler.cancel(targetId, chatId)
|
|
1131
|
+
return reply(ok ? `✅ Reminder <code>${targetId}</code> cancelled.` : `❌ Reminder <code>${targetId}</code> not found.`)
|
|
1132
|
+
}
|
|
1133
|
+
const timeArg = parts[1]
|
|
1134
|
+
const textArg = parts.slice(2).join(' ').trim()
|
|
1135
|
+
const delayMs = parseRelativeTime(timeArg)
|
|
1136
|
+
if (!delayMs || !textArg) {
|
|
1137
|
+
return reply([
|
|
1138
|
+
'⏰ <b>Reminders:</b>',
|
|
1139
|
+
'Create: <code>/remind <time> <text></code>',
|
|
1140
|
+
'Examples: <code>/remind 10m Call colleague</code>, <code>/remind 2h Check deploy</code>',
|
|
1141
|
+
'List: <code>/remind list</code>',
|
|
1142
|
+
'Cancel: <code>/remind cancel ID</code>',
|
|
1143
|
+
].join('\n'))
|
|
1144
|
+
}
|
|
1145
|
+
const dueAt = Date.now() + delayMs
|
|
1146
|
+
const task = await this.scheduler.schedule({
|
|
1147
|
+
platform: platform || 'telegram',
|
|
1148
|
+
chatId,
|
|
1149
|
+
threadId: threadId || 0,
|
|
1150
|
+
userId,
|
|
1151
|
+
text: textArg,
|
|
1152
|
+
dueAt,
|
|
1153
|
+
})
|
|
1154
|
+
const left = formatRemaining(delayMs, locale)
|
|
1155
|
+
return reply(t('remind.scheduled', { time: new Date(dueAt).toLocaleTimeString(), duration: left, text: textArg }, locale))
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
if (cmd === '/voice') {
|
|
1159
|
+
const sub = String(parts[1] || 'status').toLowerCase()
|
|
1160
|
+
if (sub === 'summary') {
|
|
1161
|
+
const val = parts[2]?.toLowerCase()
|
|
1162
|
+
if (val === 'on' || val === 'off') {
|
|
1163
|
+
if (!this.config.tts) this.config.tts = {}
|
|
1164
|
+
this.config.tts.voiceSummary = val === 'on'
|
|
1165
|
+
return reply(`Voice summary (TL;DR): ${val === 'on' ? 'enabled' : 'disabled'}`)
|
|
1166
|
+
}
|
|
1167
|
+
const state = this.config.tts?.voiceSummary ? 'on' : 'off'
|
|
1168
|
+
return reply(`Voice summary (TL;DR): ${state}\nToggle: <code>/voice summary on|off</code>`)
|
|
1169
|
+
}
|
|
1170
|
+
if (sub === 'on' || sub === 'off') {
|
|
1171
|
+
this.voicePrefs.set(userId, sub === 'on')
|
|
1172
|
+
return reply(sub === 'on' ? 'Voice replies: on (for you)' : 'Voice replies: off (for you)')
|
|
1173
|
+
}
|
|
1174
|
+
const pref = this.voicePrefs.get(userId)
|
|
1175
|
+
const mode = this.tg().voiceMode || 'mirror'
|
|
1176
|
+
const prefLine = pref === null ? 'not set (/voice on|off)' : (pref ? 'on' : 'off')
|
|
1177
|
+
const summaryState = this.config.tts?.voiceSummary ? 'on' : 'off'
|
|
1178
|
+
return reply(`voiceMode=${mode}\nyour /voice: ${prefLine}\nglobal tts: ${this.config.tts?.enabled ? 'on' : 'off'}\nvoice summary: ${summaryState}`)
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
if (cmd === '/topic') {
|
|
1182
|
+
const topicName = parts.slice(1).join(' ').trim()
|
|
1183
|
+
if (!topicName) {
|
|
1184
|
+
return reply('Usage: <code>/topic <name></code>\nCreates a new topic in supergroup with an isolated session.')
|
|
1185
|
+
}
|
|
1186
|
+
const tgAdapter = this.getAdapter('telegram')
|
|
1187
|
+
if (!tgAdapter?.createForumTopic) {
|
|
1188
|
+
return reply('Topic creation is available only in Telegram.')
|
|
1189
|
+
}
|
|
1190
|
+
try {
|
|
1191
|
+
const res = await tgAdapter.createForumTopic(chatId, topicName)
|
|
1192
|
+
const newThreadId = res?.message_thread_id
|
|
1193
|
+
await reply(`🎯 Created new topic <b>«${topicName}»</b> (ID: <code>${newThreadId}</code>).\nSwitch to the topic to continue working!`)
|
|
1194
|
+
if (newThreadId) {
|
|
1195
|
+
await tgAdapter.sendTo(chatId, {
|
|
1196
|
+
text: `👋 Hello! This is an isolated session for task <b>«${topicName}»</b>.\nHow can I help?`,
|
|
1197
|
+
}, { threadId: newThreadId })
|
|
1198
|
+
}
|
|
1199
|
+
return
|
|
1200
|
+
} catch (err) {
|
|
1201
|
+
return reply(`Failed to create topic: ${err.message}\n(Ensure the bot is group administrator with Manage Topics permission)`)
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
if (cmd === '/top') {
|
|
1206
|
+
const mem = process.memoryUsage()
|
|
1207
|
+
const rssMb = (mem.rss / 1024 / 1024).toFixed(1)
|
|
1208
|
+
const heapMb = (mem.heapUsed / 1024 / 1024).toFixed(1)
|
|
1209
|
+
const sec = Math.floor((Date.now() - this.stats.startedAt) / 1000)
|
|
1210
|
+
const hh = String(Math.floor(sec / 3600)).padStart(2, '0')
|
|
1211
|
+
const mm = String(Math.floor((sec % 3600) / 60)).padStart(2, '0')
|
|
1212
|
+
const ss = String(sec % 60).padStart(2, '0')
|
|
1213
|
+
let activeReminders = 0
|
|
1214
|
+
try { activeReminders = (await this.scheduler.list()).length } catch {}
|
|
1215
|
+
let currentModel = 'not set'
|
|
1216
|
+
try {
|
|
1217
|
+
const m = this.resolveAgentModel()
|
|
1218
|
+
currentModel = `${m.provider}/${m.model}`
|
|
1219
|
+
} catch {}
|
|
1220
|
+
|
|
1221
|
+
return reply([
|
|
1222
|
+
'📊 <b>DSH System & Resources:</b>',
|
|
1223
|
+
`• <b>Memory (RSS):</b> ${rssMb} MB`,
|
|
1224
|
+
`• <b>Heap:</b> ${heapMb} MB`,
|
|
1225
|
+
`• <b>Uptime:</b> ${hh}:${mm}:${ss}`,
|
|
1226
|
+
`• <b>Active chats:</b> ${this.chats.size}`,
|
|
1227
|
+
`• <b>Queued reminders:</b> ${activeReminders}`,
|
|
1228
|
+
`• <b>Active model:</b> <code>${currentModel}</code>`,
|
|
1229
|
+
`• <b>Messages sent:</b> ${this.stats.sent}`,
|
|
1230
|
+
`• <b>Errors:</b> ${this.stats.errors}`,
|
|
1231
|
+
].join('\n'))
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
if (cmd === '/keyboard') {
|
|
1235
|
+
const sub = String(parts[1] || '').toLowerCase()
|
|
1236
|
+
const tgAdapter = this.getAdapter('telegram')
|
|
1237
|
+
if (sub === 'on') {
|
|
1238
|
+
if (tgAdapter) tgAdapter.quickActions = true
|
|
1239
|
+
return reply('Quick action keyboard enabled.', {
|
|
1240
|
+
replyMarkup: buildQuickActionsKeyboard(),
|
|
1241
|
+
})
|
|
1242
|
+
}
|
|
1243
|
+
if (sub === 'off') {
|
|
1244
|
+
if (tgAdapter) tgAdapter.quickActions = false
|
|
1245
|
+
return reply('Quick action keyboard disabled.', {
|
|
1246
|
+
replyMarkup: REMOVE_REPLY_KEYBOARD,
|
|
1247
|
+
})
|
|
1248
|
+
}
|
|
1249
|
+
const curState = tgAdapter?.quickActions ? 'enabled' : 'disabled'
|
|
1250
|
+
return reply([
|
|
1251
|
+
'⌨️ <b>Quick Action Keyboard:</b>',
|
|
1252
|
+
`Current state: <b>${curState}</b>`,
|
|
1253
|
+
'',
|
|
1254
|
+
'Commands:',
|
|
1255
|
+
'<code>/keyboard on</code> — show buttons',
|
|
1256
|
+
'<code>/keyboard off</code> — hide buttons',
|
|
1257
|
+
].join('\n'))
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
if (cmd === '/tts') {
|
|
1261
|
+
const sub = String(parts[1] || 'status').toLowerCase()
|
|
1262
|
+
if (sub === 'on' || sub === 'off') {
|
|
1263
|
+
this.chatTts.set(chatId, sub === 'on')
|
|
1264
|
+
return reply(sub === 'on' ? 'Speech in this chat: on' : 'Speech in this chat: off')
|
|
1265
|
+
}
|
|
1266
|
+
const cur = this.chatTts.get(chatId)
|
|
1267
|
+
const line = cur === null ? 'not set (/tts on|off)' : (cur ? 'on' : 'off')
|
|
1268
|
+
return reply(`Speech in this chat: ${line}\nglobal tts: ${this.config.tts?.enabled ? 'on' : 'off'}`)
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
if (cmd === '/mute') {
|
|
1272
|
+
this.setMuted(chatId, true)
|
|
1273
|
+
return reply(t('msg.muted_on', {}, locale))
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
if (cmd === '/unmute') {
|
|
1277
|
+
this.setMuted(chatId, false)
|
|
1278
|
+
return reply(t('msg.muted_off', {}, locale))
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
return reply(t('msg.unknown_command', { cmd }, locale))
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
collectDynamicSkills() {
|
|
1285
|
+
const skills = []
|
|
1286
|
+
try {
|
|
1287
|
+
const skillsService = this.ctx.get?.('skills') || this.ctx.skills
|
|
1288
|
+
const allSkills = skillsService?.list?.() || []
|
|
1289
|
+
for (const s of allSkills) {
|
|
1290
|
+
if (!s || !s.name) continue
|
|
1291
|
+
if (s.userInvocable === false) continue
|
|
1292
|
+
skills.push({
|
|
1293
|
+
name: String(s.name),
|
|
1294
|
+
description: String(s.description || s.title || `Skill ${s.name}`).slice(0, 256),
|
|
1295
|
+
})
|
|
1296
|
+
}
|
|
1297
|
+
} catch (err) {
|
|
1298
|
+
this.ctx.logger?.debug?.(`Failed to collect dynamic skills: ${err?.message || err}`)
|
|
1299
|
+
}
|
|
1300
|
+
return skills
|
|
1301
|
+
}
|
|
1302
|
+
|
|
1303
|
+
async syncTelegramCommands() {
|
|
1304
|
+
const tgAdapter = this.adapters.get('telegram')
|
|
1305
|
+
if (!tgAdapter || typeof tgAdapter.registerCommands !== 'function') return
|
|
1306
|
+
const baseCommands = this.config.telegram?.commands || []
|
|
1307
|
+
const dynamicSkills = this.collectDynamicSkills()
|
|
1308
|
+
const merged = mergeDynamicCommands(baseCommands, dynamicSkills, 100)
|
|
1309
|
+
try {
|
|
1310
|
+
await tgAdapter.registerCommands(merged)
|
|
1311
|
+
this.ctx.logger?.info?.(`Synced ${merged.length} Telegram commands (including ${dynamicSkills.length} dynamic skills)`)
|
|
1312
|
+
} catch (err) {
|
|
1313
|
+
this.ctx.logger?.warn?.(`Failed to sync Telegram commands: ${err?.message || err}`)
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
async mirrorSessionToForumTopic(session) {
|
|
1318
|
+
if (!session || !session.id) return
|
|
1319
|
+
const sessionId = String(session.id)
|
|
1320
|
+
if (sessionId.startsWith('msgw-')) return
|
|
1321
|
+
if (this.sessionToThread.has(sessionId)) return
|
|
1322
|
+
|
|
1323
|
+
const tgCfg = this.config.telegram || {}
|
|
1324
|
+
if (!tgCfg.forumMirrorEnabled || !tgCfg.forumMirrorChatId) return
|
|
1325
|
+
|
|
1326
|
+
const tgAdapter = this.adapters.get('telegram')
|
|
1327
|
+
if (!tgAdapter || typeof tgAdapter.createForumTopic !== 'function') return
|
|
1328
|
+
|
|
1329
|
+
const forumChatId = tgCfg.forumMirrorChatId
|
|
1330
|
+
const title = String(session.title || session.meta?.name || `Session ${sessionId.slice(0, 8)}`).slice(0, 120)
|
|
1331
|
+
|
|
1332
|
+
try {
|
|
1333
|
+
const topic = await tgAdapter.createForumTopic(forumChatId, title)
|
|
1334
|
+
const threadId = topic?.message_thread_id
|
|
1335
|
+
if (!threadId) return
|
|
1336
|
+
|
|
1337
|
+
const threadKey = `${forumChatId}:${threadId}`
|
|
1338
|
+
this.sessionToThread.set(sessionId, { chatId: forumChatId, threadId })
|
|
1339
|
+
this.threadToSession.set(threadKey, sessionId)
|
|
1340
|
+
|
|
1341
|
+
const text = t('mirror.created', { sessionId, title }, 'en')
|
|
1342
|
+
await tgAdapter.sendTo(forumChatId, { text }, { threadId })
|
|
1343
|
+
this.ctx.logger?.info?.(`Mirrored session ${sessionId} to Telegram forum topic ${threadId} in ${forumChatId}`)
|
|
1344
|
+
} catch (err) {
|
|
1345
|
+
this.ctx.logger?.warn?.(`Failed to mirror session ${sessionId} to forum topic: ${err?.message || err}`)
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
async relayTurnToForumMirror(session, event) {
|
|
1350
|
+
if (!session || !session.id) return
|
|
1351
|
+
const sessionId = String(session.id)
|
|
1352
|
+
const threadInfo = this.sessionToThread.get(sessionId)
|
|
1353
|
+
if (!threadInfo) return
|
|
1354
|
+
if (this.pending.has(sessionId)) return
|
|
1355
|
+
|
|
1356
|
+
const tgAdapter = this.adapters.get('telegram')
|
|
1357
|
+
if (!tgAdapter) return
|
|
1358
|
+
|
|
1359
|
+
const messages = Array.isArray(session.messages)
|
|
1360
|
+
? session.messages
|
|
1361
|
+
: (Array.isArray(session.history) ? session.history : [])
|
|
1362
|
+
|
|
1363
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1364
|
+
const msg = messages[i]
|
|
1365
|
+
const role = msg.role || (msg.type === 'user' ? 'user' : 'assistant')
|
|
1366
|
+
if (role === 'assistant') {
|
|
1367
|
+
let text = ''
|
|
1368
|
+
if (typeof msg.content === 'string') text = msg.content
|
|
1369
|
+
else if (Array.isArray(msg.content)) {
|
|
1370
|
+
text = msg.content
|
|
1371
|
+
.map((p) => (typeof p === 'string' ? p : (p?.text || '')))
|
|
1372
|
+
.filter(Boolean)
|
|
1373
|
+
.join('\n')
|
|
1374
|
+
} else if (msg.text) text = msg.text
|
|
1375
|
+
const clean = stripReasoningPreamble(stripImageUrls(text)).trim()
|
|
1376
|
+
if (clean) {
|
|
1377
|
+
const maxLen = Number(this.config.agent?.maxMessageLength) || 4000
|
|
1378
|
+
const chunks = splitText(clean, maxLen)
|
|
1379
|
+
for (const chunk of chunks) {
|
|
1380
|
+
await tgAdapter.sendTo(threadInfo.chatId, { text: chunk }, { threadId: threadInfo.threadId })
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
break
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
async getOrCreateChat(key, input) {
|
|
1389
|
+
let chat = this.chats.get(key)
|
|
1390
|
+
if (!chat) {
|
|
1391
|
+
const threadKey = `${input.chatId}:${input.threadId || 0}`
|
|
1392
|
+
const existingSessionId = this.threadToSession.get(threadKey)
|
|
1393
|
+
chat = await this.createChat(key, input, existingSessionId)
|
|
1394
|
+
this.chats.set(key, chat)
|
|
1395
|
+
}
|
|
1396
|
+
chat.lastUsed = Date.now()
|
|
1397
|
+
chat.target = { platform: input.platform, chatId: input.chatId, threadId: input.threadId || 0 }
|
|
1398
|
+
return chat
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
resolveAgentModel() {
|
|
1402
|
+
const agentCfg = this.config.agent || {}
|
|
1403
|
+
let provider = String(agentCfg.provider || '').trim()
|
|
1404
|
+
let model = String(agentCfg.model || '').trim()
|
|
1405
|
+
if (provider && model) return { provider, model }
|
|
1406
|
+
const selection = this.ctx.get('agentDefaultModel')?.currentSelection?.()
|
|
1407
|
+
if (!selection?.provider || !selection?.model) {
|
|
1408
|
+
throw new Error('Please select a model in Settings -> Models (or set agent.provider/model in profile)')
|
|
1409
|
+
}
|
|
1410
|
+
return { provider: provider || selection.provider, model: model || selection.model }
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
async createChat(key, input, existingSessionId) {
|
|
1414
|
+
const { provider, model } = this.resolveAgentModel()
|
|
1415
|
+
const agentCfg = this.config.agent || {}
|
|
1416
|
+
const cwd = agentCfg.cwd || process.cwd()
|
|
1417
|
+
const self = this
|
|
1418
|
+
const agents = this.ctx.get?.('agents') || this.ctx.agents
|
|
1419
|
+
const targetSessionId = existingSessionId ? SessionId(existingSessionId) : SessionId(`msgw-${randomUUID()}`)
|
|
1420
|
+
const handle = await agents.create({
|
|
1421
|
+
sessionId: targetSessionId,
|
|
1422
|
+
meta: { cwd },
|
|
1423
|
+
agentOptions: { provider, model },
|
|
1424
|
+
setup: (agentCtx) => {
|
|
1425
|
+
installModelSelection(agentCtx, { current: { provider, model }, assembled: undefined })
|
|
1426
|
+
if (self.tg().approvalsEnabled !== false) {
|
|
1427
|
+
agentCtx.on('approval/request', (req, next) => self.answerApproval(key, req, next))
|
|
1428
|
+
}
|
|
1429
|
+
},
|
|
1430
|
+
})
|
|
1431
|
+
await handle.agent.whenIdle()
|
|
1432
|
+
const chat = {
|
|
1433
|
+
key, agent: handle.agent, dispose: handle.dispose, busy: Promise.resolve(),
|
|
1434
|
+
lastUsed: Date.now(), abort: undefined, pendingMedia: [], turnActive: false,
|
|
1435
|
+
sessionAllowlist: new Set(),
|
|
1436
|
+
target: input ? { platform: input.platform, chatId: input.chatId, threadId: input.threadId || 0 } : undefined,
|
|
1437
|
+
}
|
|
1438
|
+
this.sessionToChat.set(String(handle.agent.session.id), key)
|
|
1439
|
+
if (existingSessionId && input) {
|
|
1440
|
+
const threadKey = `${input.chatId}:${input.threadId || 0}`
|
|
1441
|
+
this.threadToSession.set(threadKey, existingSessionId)
|
|
1442
|
+
this.sessionToThread.set(existingSessionId, { chatId: input.chatId, threadId: input.threadId || 0 })
|
|
1443
|
+
}
|
|
1444
|
+
return chat
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
async answerApproval(chatKeyValue, req, next) {
|
|
1448
|
+
try {
|
|
1449
|
+
const chat = this.chats.get(chatKeyValue)
|
|
1450
|
+
if (!chat?.target) return next()
|
|
1451
|
+
const tool = req.toolName || 'tool'
|
|
1452
|
+
if (chat.sessionAllowlist?.has(tool)) {
|
|
1453
|
+
return 'allowed-once'
|
|
1454
|
+
}
|
|
1455
|
+
const locale = this.resolveLocale(chat.target)
|
|
1456
|
+
const reason = req.reason ? `\n<i>${req.reason}</i>` : ''
|
|
1457
|
+
let detail = ''
|
|
1458
|
+
if (req.input && typeof req.input === 'object') {
|
|
1459
|
+
try {
|
|
1460
|
+
const jsonStr = JSON.stringify(req.input, null, 2)
|
|
1461
|
+
detail = `\n<pre><code>${jsonStr.slice(0, 500)}</code></pre>`
|
|
1462
|
+
} catch {}
|
|
1463
|
+
}
|
|
1464
|
+
const text = t('ask.confirm_title', { tool, reason: reason + detail }, locale)
|
|
1465
|
+
const buttons = [
|
|
1466
|
+
[
|
|
1467
|
+
{ id: 'allow_once', text: t('ask.allow_once', {}, locale) },
|
|
1468
|
+
{ id: 'allow_session', text: t('ask.allow_session', {}, locale) },
|
|
1469
|
+
{ id: 'deny', text: t('ask.deny', {}, locale) },
|
|
1470
|
+
],
|
|
1471
|
+
]
|
|
1472
|
+
const result = await this.messengerAsk(chat.target, { text, buttons }, 300_000)
|
|
1473
|
+
if (result?.buttonId === 'allow_once' || result?.buttonId === 'allow') return 'allowed-once'
|
|
1474
|
+
if (result?.buttonId === 'allow_session') {
|
|
1475
|
+
if (!chat.sessionAllowlist) chat.sessionAllowlist = new Set()
|
|
1476
|
+
chat.sessionAllowlist.add(tool)
|
|
1477
|
+
return 'allowed-once'
|
|
1478
|
+
}
|
|
1479
|
+
if (result?.buttonId === 'deny') return 'rejected'
|
|
1480
|
+
return next()
|
|
1481
|
+
} catch {
|
|
1482
|
+
return next()
|
|
1483
|
+
}
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
async buildUserContent(input, signal) {
|
|
1487
|
+
const { text, attachments = [], replyText, steer, personaOverride } = input
|
|
1488
|
+
const parts = []
|
|
1489
|
+
parts.push(String(this.config.agent?.instructionPrefix || MESSENGER_RELAY_INSTRUCTION))
|
|
1490
|
+
const activePersonaId = personaOverride || this.personas.getPersonaForChat(input.chatId, input.threadId)
|
|
1491
|
+
const activePersona = getPersona(activePersonaId)
|
|
1492
|
+
if (activePersona?.instruction) {
|
|
1493
|
+
parts.push(`[Persona: ${activePersona.name} (${activePersona.icon})]\n${activePersona.instruction}`)
|
|
1494
|
+
}
|
|
1495
|
+
if (steer) parts.push('[Steer / addition to current turn: combine with previous instruction, do not restart from scratch]')
|
|
1496
|
+
if (replyText?.trim()) parts.push(`[Replying to message: ${replyText.trim()}]`)
|
|
1497
|
+
const blocks = []
|
|
1498
|
+
for (const att of attachments) {
|
|
1499
|
+
if (att.kind === 'photo' || (att.kind === 'sticker' && att.mime?.startsWith('image/'))) {
|
|
1500
|
+
try {
|
|
1501
|
+
const { ref } = await attachInboundPhoto(this.ctx, att, {
|
|
1502
|
+
signal,
|
|
1503
|
+
maxBytes: Number(this.config.media?.maxImageBytes) || 20 * 1024 * 1024,
|
|
1504
|
+
})
|
|
1505
|
+
blocks.push({ type: 'image', attachment: ref })
|
|
1506
|
+
if (att.kind === 'sticker' && att.emoji) parts.push(`[Sticker ${att.emoji}]`)
|
|
1507
|
+
} catch (err) {
|
|
1508
|
+
if (signal?.aborted) throw err
|
|
1509
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
1510
|
+
parts.push(`[Failed to attach image: ${msg}]`)
|
|
1511
|
+
}
|
|
1512
|
+
} else if (att.kind === 'voice' || att.kind === 'audio') {
|
|
1513
|
+
try {
|
|
1514
|
+
const bytes = new Uint8Array(await readFile(att.path))
|
|
1515
|
+
const transcript = await transcribeVoice(this.baseUrl(), bytes, att.mime || 'audio/ogg', 'message', signal)
|
|
1516
|
+
parts.push(transcript ? `[Voice message transcript: ${transcript}]` : '[Voice message (unrecognized)]')
|
|
1517
|
+
} catch (err) {
|
|
1518
|
+
if (signal?.aborted) throw err
|
|
1519
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
1520
|
+
this.ctx.logger?.warn?.(`voice: ${msg}`)
|
|
1521
|
+
parts.push(`[Voice message (dsh-voice unavailable: ${msg})]`)
|
|
1522
|
+
}
|
|
1523
|
+
} else if (att.kind === 'document' || att.kind === 'video' || att.kind === 'animation' || att.kind === 'sticker') {
|
|
1524
|
+
let parsed = null
|
|
1525
|
+
if (att.kind === 'document' && att.path) {
|
|
1526
|
+
try {
|
|
1527
|
+
const maxDocBytes = Number(this.config.media?.maxTextInjectBytes) || 100 * 1024
|
|
1528
|
+
parsed = await parseDocument(att.path, { maxBytes: maxDocBytes })
|
|
1529
|
+
} catch {}
|
|
1530
|
+
}
|
|
1531
|
+
parts.push(formatInboundDocument(att, parsed))
|
|
1532
|
+
} else {
|
|
1533
|
+
parts.push(`[File: ${att.path}${att.name ? ` (${att.name})` : ''}]`)
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
const photoHint = photoOnlyHint(attachments, text)
|
|
1537
|
+
if (photoHint) parts.push(photoHint)
|
|
1538
|
+
const docHint = documentOnlyHint(attachments, text)
|
|
1539
|
+
if (docHint) parts.push(docHint)
|
|
1540
|
+
if (text?.trim()) parts.push(text.trim())
|
|
1541
|
+
const textBlock = parts.filter(Boolean).join('\n\n')
|
|
1542
|
+
if (textBlock) blocks.unshift({ type: 'text', text: textBlock })
|
|
1543
|
+
if (!blocks.length) blocks.push({ type: 'text', text: '(empty message)' })
|
|
1544
|
+
return blocks
|
|
1545
|
+
}
|
|
1546
|
+
|
|
1547
|
+
async runTurn(chat, input, signal) {
|
|
1548
|
+
const { reply, typing, startStream, startProgress, react, inboundWasVoice, userId } = input
|
|
1549
|
+
chat.turnActive = true
|
|
1550
|
+
const sessionId = chat.agent.session.id
|
|
1551
|
+
const tg = this.tg()
|
|
1552
|
+
const streaming = tg.streaming === true && typeof startStream === 'function'
|
|
1553
|
+
const progressEnabled = tg.progressEnabled !== false
|
|
1554
|
+
const collector = { parts: [], lastText: '', streamText: '', toolName: '', images: [], reason: undefined, onStream: undefined }
|
|
1555
|
+
this.pending.set(sessionId, collector)
|
|
1556
|
+
let stopTyping = () => {}
|
|
1557
|
+
let stream = null
|
|
1558
|
+
let scheduler = null
|
|
1559
|
+
let progress = null
|
|
1560
|
+
try {
|
|
1561
|
+
if (signal.aborted) return
|
|
1562
|
+
if (typeof react === 'function' && tg.reactionsEnabled !== false) {
|
|
1563
|
+
react('👀').catch?.(() => {})
|
|
1564
|
+
}
|
|
1565
|
+
if (typeof typing === 'function') stopTyping = startTypingHeartbeat(typing, 4000)
|
|
1566
|
+
if (streaming) {
|
|
1567
|
+
try {
|
|
1568
|
+
stream = await startStream()
|
|
1569
|
+
scheduler = createEditScheduler((text) => stream.edit(text), Number(tg.streamEditIntervalMs) || 1200)
|
|
1570
|
+
collector.onStream = (text, toolName) => {
|
|
1571
|
+
if (text) stopTyping()
|
|
1572
|
+
if (!progressEnabled && !text) return
|
|
1573
|
+
scheduler.push(buildStreamPreview(text, progressEnabled ? toolName : ''))
|
|
1574
|
+
}
|
|
1575
|
+
if (progressEnabled) scheduler.push(buildStreamPreview('', ''))
|
|
1576
|
+
} catch (e) {
|
|
1577
|
+
this.ctx.logger?.warn?.(`stream start: ${e.message}`)
|
|
1578
|
+
stream = null
|
|
1579
|
+
}
|
|
1580
|
+
} else if (progressEnabled && typeof startProgress === 'function') {
|
|
1581
|
+
try {
|
|
1582
|
+
progress = await startProgress()
|
|
1583
|
+
const editProgress = createEditScheduler((text) => progress.edit(text), 800)
|
|
1584
|
+
collector.onStream = (_text, toolName) => {
|
|
1585
|
+
editProgress.push(formatProgressLine(toolName))
|
|
1586
|
+
}
|
|
1587
|
+
} catch (e) {
|
|
1588
|
+
this.ctx.logger?.warn?.(`progress start: ${e.message}`)
|
|
1589
|
+
progress = null
|
|
1590
|
+
}
|
|
1591
|
+
}
|
|
1592
|
+
|
|
1593
|
+
|
|
1594
|
+
const content = await this.buildUserContent(input, signal)
|
|
1595
|
+
chat.agent.followup(createUserMessage({
|
|
1596
|
+
content: ensureContentArray(content),
|
|
1597
|
+
source: { kind: 'user', plugin: PLUGIN, form: 'relay', origin: 'telegram' },
|
|
1598
|
+
}))
|
|
1599
|
+
const turnTimeoutMs = Number(this.config.agent?.turnTimeoutMs) || 600_000
|
|
1600
|
+
await whenIdleWithTimeout(chat.agent, turnTimeoutMs, signal)
|
|
1601
|
+
if (signal.aborted) {
|
|
1602
|
+
if (progress) try { await progress.remove() } catch {}
|
|
1603
|
+
if (typeof react === 'function') react('').catch?.(() => {})
|
|
1604
|
+
const stoppedMsg = t('msg.turn_stopped', {}, this.resolveLocale(input))
|
|
1605
|
+
if (stream) try { await stream.finalize(stoppedMsg) } catch {}
|
|
1606
|
+
else return reply(stoppedMsg)
|
|
1607
|
+
return
|
|
1608
|
+
}
|
|
1609
|
+
const sessions = this.ctx.get?.('sessions') || this.ctx.sessions
|
|
1610
|
+
await sessions?.flush?.(chat.agent.session)
|
|
1611
|
+
if (progress) try { await progress.remove() } catch {}
|
|
1612
|
+
progress = null
|
|
1613
|
+
if (typeof react === 'function') react('').catch?.(() => {})
|
|
1614
|
+
const rawAnswer = stripReasoningPreamble(stripImageUrls(collector.lastText || collector.streamText || collector.parts.join('\n\n')))
|
|
1615
|
+
const processed = processDiagramsAndTables(rawAnswer, {
|
|
1616
|
+
artifactPreviews: this.tg().artifactPreviews !== false,
|
|
1617
|
+
})
|
|
1618
|
+
const answer = processed.text
|
|
1619
|
+
if (collector.reason?.kind === 'error') {
|
|
1620
|
+
const err = collector.reason.error
|
|
1621
|
+
const msg = t('msg.agent_error', { code: err?.code || 'error', message: err?.message || 'unknown' }, this.resolveLocale(input))
|
|
1622
|
+
this.sendAlert('error', {
|
|
1623
|
+
code: err?.code || 'AGENT_ERROR',
|
|
1624
|
+
message: err?.message || 'unknown',
|
|
1625
|
+
sessionId,
|
|
1626
|
+
chatId: input.chatId,
|
|
1627
|
+
threadId: input.threadId,
|
|
1628
|
+
}).catch(() => {})
|
|
1629
|
+
if (stream) { await scheduler?.flush(); await stream.finalize(msg) }
|
|
1630
|
+
else await reply(msg)
|
|
1631
|
+
return
|
|
1632
|
+
}
|
|
1633
|
+
const files = await buildOutboundFiles(this.ctx, this.baseUrl(), collector, { signal, logger: this.ctx.logger })
|
|
1634
|
+
const allFiles = [...files, ...(processed.files || [])]
|
|
1635
|
+
if (!answer && !allFiles.length) {
|
|
1636
|
+
const noResp = t('msg.no_response', {}, this.resolveLocale(input))
|
|
1637
|
+
if (stream) { await scheduler?.flush(); await stream.finalize(noResp) }
|
|
1638
|
+
else await reply(noResp)
|
|
1639
|
+
return
|
|
1640
|
+
}
|
|
1641
|
+
const maxLen = Number(this.config.agent?.maxMessageLength) || 4000
|
|
1642
|
+
const chunks = answer ? splitText(answer, maxLen) : ['']
|
|
1643
|
+
if (stream) {
|
|
1644
|
+
await scheduler?.flush()
|
|
1645
|
+
await stream.finalize(chunks[0] || t('msg.no_response', {}, this.resolveLocale(input)))
|
|
1646
|
+
for (let i = 1; i < chunks.length; i++) await reply({ text: chunks[i] })
|
|
1647
|
+
if (allFiles.length) await reply({ files: allFiles })
|
|
1648
|
+
} else {
|
|
1649
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
1650
|
+
await reply({ text: chunks[i] || undefined, files: i === 0 ? allFiles : [] })
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
const chatTtsPref = this.chatTts.get(chat.target?.chatId)
|
|
1654
|
+
const speak = shouldSpeakReply({
|
|
1655
|
+
globalTts: Boolean(this.config.tts?.enabled),
|
|
1656
|
+
voiceMode: this.tg().voiceMode || 'mirror',
|
|
1657
|
+
inboundWasVoice: Boolean(inboundWasVoice),
|
|
1658
|
+
userPref: this.voicePrefs.get(userId),
|
|
1659
|
+
chatPref: chatTtsPref,
|
|
1660
|
+
})
|
|
1661
|
+
if (speak && !signal.aborted) {
|
|
1662
|
+
const isVoiceSummary = this.config.tts?.voiceSummary === true
|
|
1663
|
+
const ttsText = prepareTtsText(answer, this.config.tts?.maxChars, { voiceSummary: isVoiceSummary })
|
|
1664
|
+
if (ttsText) {
|
|
1665
|
+
try {
|
|
1666
|
+
const spoken = await speakText(this.baseUrl(), ttsText, signal)
|
|
1667
|
+
const voiceFile = await toTelegramVoiceFile(spoken, { logger: this.ctx.logger })
|
|
1668
|
+
if (!signal.aborted && voiceFile) await reply({ files: [voiceFile] })
|
|
1669
|
+
} catch (e) {
|
|
1670
|
+
if (!signal?.aborted) this.ctx.logger?.warn?.(`tts: ${e.message}`)
|
|
1671
|
+
}
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
} catch (err) {
|
|
1675
|
+
if (!signal?.aborted) {
|
|
1676
|
+
this.sendAlert('error', {
|
|
1677
|
+
code: err?.code || 'EXCEPTION',
|
|
1678
|
+
message: err?.message || String(err),
|
|
1679
|
+
sessionId,
|
|
1680
|
+
chatId: input.chatId,
|
|
1681
|
+
threadId: input.threadId,
|
|
1682
|
+
}).catch(() => {})
|
|
1683
|
+
try {
|
|
1684
|
+
const excMsg = t('msg.exception', { message: err.message }, this.resolveLocale(input))
|
|
1685
|
+
if (stream) await stream.finalize(excMsg)
|
|
1686
|
+
else await reply(excMsg)
|
|
1687
|
+
} catch {}
|
|
1688
|
+
}
|
|
1689
|
+
} finally {
|
|
1690
|
+
stopTyping()
|
|
1691
|
+
if (progress) try { await progress.remove() } catch {}
|
|
1692
|
+
if (typeof react === 'function') react('').catch?.(() => {})
|
|
1693
|
+
chat.turnActive = false
|
|
1694
|
+
chat.abort = undefined
|
|
1695
|
+
this.pending.delete(sessionId)
|
|
1696
|
+
}
|
|
1697
|
+
}
|
|
1698
|
+
|
|
1699
|
+
reapIdle() {
|
|
1700
|
+
const rawTimeout = Number(this.config.agent?.idleTimeoutMs)
|
|
1701
|
+
const timeout = Number.isFinite(rawTimeout) && rawTimeout > 0 ? rawTimeout : 86_400_000
|
|
1702
|
+
const now = Date.now()
|
|
1703
|
+
for (const [key, chat] of this.chats) {
|
|
1704
|
+
if (!chat.turnActive && now - chat.lastUsed > timeout) {
|
|
1705
|
+
if (chat.agent?.session?.id) {
|
|
1706
|
+
this.sessionToChat.delete(String(chat.agent.session.id))
|
|
1707
|
+
}
|
|
1708
|
+
this.chats.delete(key)
|
|
1709
|
+
chat.dispose().catch(() => {})
|
|
1710
|
+
}
|
|
1711
|
+
}
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
async approvePairingCode(code, actorUserId = 0) {
|
|
1715
|
+
const res = this.pairing.approveCode(code, actorUserId)
|
|
1716
|
+
if (!res.ok) return res
|
|
1717
|
+
const merged = this.effectiveAllowedIds()
|
|
1718
|
+
for (const a of this.adapterList) a.setAllowedUserIds?.(merged)
|
|
1719
|
+
try { await this.hooks?.persistAllowedUserIds?.(merged) } catch (e) {
|
|
1720
|
+
this.ctx.logger?.warn?.(`persist allowlist: ${e.message}`)
|
|
1721
|
+
}
|
|
1722
|
+
return { ...res, allowedUserIds: merged }
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
rejectPairingCode(code) {
|
|
1726
|
+
return this.pairing.rejectCode(code)
|
|
1727
|
+
}
|
|
1728
|
+
|
|
1729
|
+
async probeTelegram(timeoutMs = 10000) {
|
|
1730
|
+
const adapter = this.getAdapter('telegram')
|
|
1731
|
+
if (!adapter) {
|
|
1732
|
+
return { ok: false, error: 'Telegram adapter not initialized' }
|
|
1733
|
+
}
|
|
1734
|
+
if (typeof adapter.probeHealth === 'function') {
|
|
1735
|
+
return adapter.probeHealth(timeoutMs)
|
|
1736
|
+
}
|
|
1737
|
+
return { ok: false, error: 'probeHealth not implemented on adapter' }
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1740
|
+
getBotInfo() {
|
|
1741
|
+
const adapter = this.getAdapter('telegram')
|
|
1742
|
+
return {
|
|
1743
|
+
botId: adapter?.botId || 0,
|
|
1744
|
+
botUsername: adapter?.botUsername || '',
|
|
1745
|
+
pollingConflict: Boolean(adapter?.pollingConflict),
|
|
1746
|
+
}
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
async messengerAskFromAgent(agent, payload, timeoutMs) {
|
|
1750
|
+
const sessionId = String(agent?.session?.id || '')
|
|
1751
|
+
const key = this.sessionToChat.get(sessionId)
|
|
1752
|
+
const chat = key ? this.chats.get(key) : null
|
|
1753
|
+
if (!chat?.target) throw new Error('messenger_ask: no telegram chat for this agent session')
|
|
1754
|
+
return this.messengerAsk(chat.target, payload, timeoutMs)
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
get messenger() {
|
|
1758
|
+
return {
|
|
1759
|
+
adapters: () => [...this.adapters.keys()],
|
|
1760
|
+
activeChats: () => this.chats.size,
|
|
1761
|
+
home: (name) => this.resolveHomeTarget('telegram', name),
|
|
1762
|
+
homes: () => listHomes(this.tg()),
|
|
1763
|
+
pairingPending: () => this.pairing.listPending(),
|
|
1764
|
+
pairingApproved: () => this.pairing.listApproved(),
|
|
1765
|
+
send: (target, payload) => this.messengerSend(target, payload),
|
|
1766
|
+
ask: (target, payload, timeoutMs) => this.messengerAsk(target, payload, timeoutMs),
|
|
1767
|
+
progress: (target, payload) => this.messengerProgress(target, payload),
|
|
1768
|
+
probeTelegram: (timeoutMs) => this.probeTelegram(timeoutMs),
|
|
1769
|
+
getBotInfo: () => this.getBotInfo(),
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
}
|