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