@goodandready/dsh-messenger-gateway 0.3.20 → 0.3.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -2
- package/README.ru.md +10 -0
- package/README.zh.md +10 -0
- package/lib/adapters/discord.js +65 -11
- package/lib/adapters/slack.js +72 -12
- package/lib/adapters/telegram.js +9 -9
- package/lib/alerts.js +10 -10
- package/lib/artifacts.js +117 -117
- package/lib/ask.js +151 -151
- package/lib/client.js +104 -104
- package/lib/commands.js +33 -30
- package/lib/config.js +1 -1
- package/lib/content-guard.js +14 -14
- package/lib/documents.js +9 -9
- package/lib/file-manager.js +9 -9
- package/lib/gateway.js +395 -197
- package/lib/index.js +1 -1
- package/lib/locales/en.js +114 -0
- package/lib/locales/index.js +21 -0
- package/lib/locales/zh.js +114 -0
- package/lib/models.js +1 -1
- package/lib/personas.js +67 -14
- package/lib/photos.js +2 -2
- package/lib/scheduler.js +159 -142
- package/lib/stream.js +4 -2
- package/lib/telegram-errors.js +0 -0
- package/lib/text.js +39 -39
- package/package.json +1 -1
package/lib/gateway.js
CHANGED
|
@@ -35,6 +35,7 @@ import {
|
|
|
35
35
|
import { isTopicGoneError } from './telegram-errors.js'
|
|
36
36
|
import { ensureContentArray } from './content-guard.js'
|
|
37
37
|
import { mergeDynamicCommands } from './commands.js'
|
|
38
|
+
import { t } from './locales/index.js'
|
|
38
39
|
|
|
39
40
|
|
|
40
41
|
function whenIdleWithTimeout(agent, timeoutMs, signal) {
|
|
@@ -78,18 +79,74 @@ export class Gateway {
|
|
|
78
79
|
this.chatTts = createVoicePrefs(join(home, 'messenger-gateway', 'chat-tts.json'))
|
|
79
80
|
this.muted = createVoicePrefs(join(home, 'messenger-gateway', 'muted.json'))
|
|
80
81
|
this.personas = createPersonaStore(join(home, 'messenger-gateway', 'personas.json'))
|
|
82
|
+
this.chatLocales = createVoicePrefs(join(home, 'messenger-gateway', 'chat-locales.json'))
|
|
81
83
|
this.scheduler = createScheduler(join(home, 'messenger-gateway', 'scheduled.json'), async (task) => {
|
|
82
84
|
const target = {
|
|
83
85
|
platform: task.platform || 'telegram',
|
|
84
86
|
chatId: task.chatId,
|
|
85
87
|
threadId: task.threadId || 0,
|
|
86
88
|
}
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
+
const locale = this.resolveLocale({ chatId: task.chatId })
|
|
90
|
+
if (task.prompt || task.action === 'prompt') {
|
|
91
|
+
const promptText = task.prompt || task.text
|
|
92
|
+
try {
|
|
93
|
+
await this.dispatchAutonomousPrompt(target, promptText, locale)
|
|
94
|
+
} catch (err) {
|
|
95
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
96
|
+
this.ctx.logger?.warn?.(`dsh-messenger-gateway: cron prompt error: ${msg}`)
|
|
97
|
+
await this.sendToMessenger(target, {
|
|
98
|
+
text: `⚠️ <b>[Cron Error]</b>\n${msg}`,
|
|
99
|
+
}).catch(() => {})
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
const text = t('remind.prefix', { text: task.text }, locale)
|
|
103
|
+
await this.sendToMessenger(target, { text })
|
|
104
|
+
}
|
|
89
105
|
})
|
|
90
106
|
this.stats = { sent: 0, errors: 0, startedAt: Date.now() }
|
|
91
107
|
}
|
|
92
108
|
|
|
109
|
+
resolveLocale(input) {
|
|
110
|
+
if (input?.locale) return input.locale
|
|
111
|
+
const chatId = input?.chatId
|
|
112
|
+
if (chatId && this.chatLocales?.get(chatId)) return this.chatLocales.get(chatId)
|
|
113
|
+
if (input?.languageCode) {
|
|
114
|
+
const code = String(input.languageCode).toLowerCase()
|
|
115
|
+
if (code.startsWith('zh')) return 'zh'
|
|
116
|
+
if (code.startsWith('en')) return 'en'
|
|
117
|
+
}
|
|
118
|
+
return this.config?.defaultLocale || 'en'
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async dispatchAutonomousPrompt(target, promptText, locale = 'en') {
|
|
122
|
+
const key = this.sessionKeyFor(target)
|
|
123
|
+
const reply = async (payload) => {
|
|
124
|
+
await this.sendToMessenger(target, typeof payload === 'string' ? { text: payload } : payload)
|
|
125
|
+
}
|
|
126
|
+
const input = {
|
|
127
|
+
platform: target.platform || 'telegram',
|
|
128
|
+
chatId: target.chatId,
|
|
129
|
+
threadId: target.threadId || 0,
|
|
130
|
+
text: promptText,
|
|
131
|
+
reply,
|
|
132
|
+
locale,
|
|
133
|
+
}
|
|
134
|
+
const chat = await this.getOrCreateChat(key, input)
|
|
135
|
+
const turnInput = { ...input, attachments: [], inboundWasVoice: false }
|
|
136
|
+
chat.turnActive = true
|
|
137
|
+
try { chat.abort?.abort?.() } catch {}
|
|
138
|
+
chat.abort = new AbortController()
|
|
139
|
+
const run = chat.busy.then(() => this.runTurn(chat, turnInput, chat.abort.signal))
|
|
140
|
+
chat.busy = run.catch(() => {})
|
|
141
|
+
run.catch((err) => {
|
|
142
|
+
const msg = err instanceof Error ? err.message : String(err)
|
|
143
|
+
this.ctx.logger?.warn?.(`dsh-messenger-gateway: cron turn: ${msg}`)
|
|
144
|
+
chat.turnActive = false
|
|
145
|
+
chat.abort = undefined
|
|
146
|
+
})
|
|
147
|
+
return run
|
|
148
|
+
}
|
|
149
|
+
|
|
93
150
|
isMuted(chatId) { return this.muted.get(chatId) === true }
|
|
94
151
|
setMuted(chatId, on) { return this.muted.set(chatId, on) }
|
|
95
152
|
|
|
@@ -300,23 +357,24 @@ export class Gateway {
|
|
|
300
357
|
|
|
301
358
|
async handleUnauthorized(input) {
|
|
302
359
|
const { reply, userId, username } = input
|
|
360
|
+
const locale = this.resolveLocale(input)
|
|
303
361
|
if (!this.tg().pairingEnabled) {
|
|
304
|
-
await reply('
|
|
362
|
+
await reply(t('msg.not_allowed', {}, locale))
|
|
305
363
|
return
|
|
306
364
|
}
|
|
307
365
|
try {
|
|
308
366
|
const { code } = this.pairing.requestCode(userId, { username })
|
|
309
|
-
await reply(
|
|
367
|
+
await reply(t('msg.pairing_requested', { userId, code }, locale))
|
|
310
368
|
this.sendAlert('pairing', { userId, username, code }).catch(() => {})
|
|
311
369
|
} catch (err) {
|
|
312
|
-
if (err.code === 'RATE_LIMIT') await reply('
|
|
313
|
-
else await reply(
|
|
370
|
+
if (err.code === 'RATE_LIMIT') await reply(t('msg.pairing_rate_limit', {}, locale))
|
|
371
|
+
else await reply(t('msg.exception', { message: err.message }, locale))
|
|
314
372
|
}
|
|
315
373
|
}
|
|
316
374
|
|
|
317
375
|
async handleCallback(cb) {
|
|
318
376
|
if (cb.userId && !this.isUserAllowed(cb.userId)) {
|
|
319
|
-
try { await cb.answer('
|
|
377
|
+
try { await cb.answer(t('msg.not_allowed', {}, 'en')) } catch {}
|
|
320
378
|
return
|
|
321
379
|
}
|
|
322
380
|
const indexed = this.callbackIndex.get(cb.data)
|
|
@@ -325,7 +383,7 @@ export class Gateway {
|
|
|
325
383
|
if (askToken && this.pendingAsks.has(askToken)) {
|
|
326
384
|
const pending = this.pendingAsks.get(askToken)
|
|
327
385
|
if (!targetMatchesAsk(pending, cb)) {
|
|
328
|
-
await cb.answer('
|
|
386
|
+
await cb.answer(t('ask.other_chat', {}, 'en'))
|
|
329
387
|
return
|
|
330
388
|
}
|
|
331
389
|
const action = parseAskCallback(cb.data)
|
|
@@ -339,9 +397,9 @@ export class Gateway {
|
|
|
339
397
|
indexCallbacks(this.callbackIndex, nextKb.callbackKeys, askToken)
|
|
340
398
|
try {
|
|
341
399
|
if (cb.editReplyMarkup) await cb.editReplyMarkup(nextKb.replyMarkup)
|
|
342
|
-
else await cb.editMessage(cb.message?.text || '
|
|
400
|
+
else await cb.editMessage(cb.message?.text || 'Selection', nextKb.replyMarkup)
|
|
343
401
|
} catch {}
|
|
344
|
-
await cb.answer(pending.selected.has(action.id) ? '
|
|
402
|
+
await cb.answer(pending.selected.has(action.id) ? 'Selected' : 'Deselected')
|
|
345
403
|
return
|
|
346
404
|
}
|
|
347
405
|
if (action.kind === 'page') {
|
|
@@ -352,7 +410,7 @@ export class Gateway {
|
|
|
352
410
|
indexCallbacks(this.callbackIndex, nextKb.callbackKeys, askToken)
|
|
353
411
|
try {
|
|
354
412
|
if (cb.editReplyMarkup) await cb.editReplyMarkup(nextKb.replyMarkup)
|
|
355
|
-
else await cb.editMessage(cb.message?.text || '
|
|
413
|
+
else await cb.editMessage(cb.message?.text || 'Selection', nextKb.replyMarkup)
|
|
356
414
|
} catch {}
|
|
357
415
|
await cb.answer()
|
|
358
416
|
return
|
|
@@ -362,7 +420,7 @@ export class Gateway {
|
|
|
362
420
|
clearTimeout(pending.timer)
|
|
363
421
|
releaseCallbacks(this.callbackIndex, pending.callbackKeys)
|
|
364
422
|
await cb.answer('OK')
|
|
365
|
-
try { await cb.editMessage(cb.message?.text || '
|
|
423
|
+
try { await cb.editMessage(cb.message?.text || 'Done', REMOVE_KEYBOARD) } catch {}
|
|
366
424
|
pending.resolve({ buttonId: 'done', selected: Array.from(pending.selected), data: cb.data })
|
|
367
425
|
return
|
|
368
426
|
}
|
|
@@ -370,8 +428,8 @@ export class Gateway {
|
|
|
370
428
|
this.pendingAsks.delete(askToken)
|
|
371
429
|
clearTimeout(pending.timer)
|
|
372
430
|
releaseCallbacks(this.callbackIndex, pending.callbackKeys)
|
|
373
|
-
await cb.answer('
|
|
374
|
-
try { await cb.editMessage(cb.message?.text || '
|
|
431
|
+
await cb.answer('Cancelled')
|
|
432
|
+
try { await cb.editMessage(cb.message?.text || 'Cancelled', REMOVE_KEYBOARD) } catch {}
|
|
375
433
|
pending.resolve({ buttonId: 'cancel', selected: [], data: cb.data })
|
|
376
434
|
return
|
|
377
435
|
}
|
|
@@ -380,7 +438,7 @@ export class Gateway {
|
|
|
380
438
|
clearTimeout(pending.timer)
|
|
381
439
|
releaseCallbacks(this.callbackIndex, pending.callbackKeys)
|
|
382
440
|
await cb.answer('OK')
|
|
383
|
-
try { await cb.editMessage(cb.message?.text || '
|
|
441
|
+
try { await cb.editMessage(cb.message?.text || 'Done', REMOVE_KEYBOARD) } catch {}
|
|
384
442
|
pending.resolve({ buttonId: action.id || buttonId, data: cb.data })
|
|
385
443
|
return
|
|
386
444
|
}
|
|
@@ -396,14 +454,14 @@ export class Gateway {
|
|
|
396
454
|
const catalog = await listModelCatalog(this.ctx, current)
|
|
397
455
|
const models = catalog.modelsByProvider.get(providerId) || []
|
|
398
456
|
if (!models.length) {
|
|
399
|
-
await cb.answer('
|
|
457
|
+
await cb.answer(t('model.no_models', {}, 'en'))
|
|
400
458
|
return
|
|
401
459
|
}
|
|
402
460
|
const kb = buildModelsKeyboard(providerId, models, current.model, 0)
|
|
403
461
|
await cb.answer()
|
|
404
462
|
const text = [
|
|
405
|
-
`🤖 <b
|
|
406
|
-
|
|
463
|
+
`🤖 <b>Provider:</b> <code>${providerId}</code>`,
|
|
464
|
+
`Select model (page ${kb.page + 1}/${kb.totalPages}):`,
|
|
407
465
|
].join('\n')
|
|
408
466
|
try {
|
|
409
467
|
if (cb.editMessage) await cb.editMessage(text, kb)
|
|
@@ -420,8 +478,8 @@ export class Gateway {
|
|
|
420
478
|
const kb = buildModelsKeyboard(providerId, models, current.model, page)
|
|
421
479
|
await cb.answer()
|
|
422
480
|
const text = [
|
|
423
|
-
`🤖 <b
|
|
424
|
-
|
|
481
|
+
`🤖 <b>Provider:</b> <code>${providerId}</code>`,
|
|
482
|
+
`Select model (page ${kb.page + 1}/${kb.totalPages}):`,
|
|
425
483
|
].join('\n')
|
|
426
484
|
try {
|
|
427
485
|
if (cb.editMessage) await cb.editMessage(text, kb)
|
|
@@ -435,8 +493,8 @@ export class Gateway {
|
|
|
435
493
|
const kb = buildProvidersKeyboard(catalog.providers, current)
|
|
436
494
|
await cb.answer()
|
|
437
495
|
const text = [
|
|
438
|
-
'🤖 <b
|
|
439
|
-
|
|
496
|
+
'🤖 <b>Choose Provider:</b>',
|
|
497
|
+
`Current: <code>${current.provider}/${current.model}</code>`,
|
|
440
498
|
].join('\n')
|
|
441
499
|
try {
|
|
442
500
|
if (cb.editMessage) await cb.editMessage(text, kb)
|
|
@@ -448,7 +506,7 @@ export class Gateway {
|
|
|
448
506
|
const key = parts[2]
|
|
449
507
|
const stored = getStoredModelSelection(key)
|
|
450
508
|
if (!stored) {
|
|
451
|
-
await cb.answer('
|
|
509
|
+
await cb.answer(t('ask.expired', {}, 'en'))
|
|
452
510
|
return
|
|
453
511
|
}
|
|
454
512
|
const { provider, model } = stored
|
|
@@ -463,12 +521,12 @@ export class Gateway {
|
|
|
463
521
|
} catch (e) {
|
|
464
522
|
this.ctx.logger?.warn?.(`persist agent model: ${e.message}`)
|
|
465
523
|
}
|
|
466
|
-
await cb.answer(
|
|
524
|
+
await cb.answer(t('ask.chose', { choice: model }, 'en'))
|
|
467
525
|
try {
|
|
468
|
-
if (cb.editMessage) await cb.editMessage(
|
|
526
|
+
if (cb.editMessage) await cb.editMessage(t('model.switched', { provider, model }, 'en'), REMOVE_KEYBOARD)
|
|
469
527
|
} catch {}
|
|
470
528
|
} catch (err) {
|
|
471
|
-
await cb.answer(
|
|
529
|
+
await cb.answer(`Error: ${err.message}`)
|
|
472
530
|
}
|
|
473
531
|
return
|
|
474
532
|
}
|
|
@@ -512,9 +570,10 @@ export class Gateway {
|
|
|
512
570
|
if (incomingPhotoOnly && photoOnlyMode === 'prompt') {
|
|
513
571
|
chat.pendingMedia = [...(chat.pendingMedia || []), ...attachments]
|
|
514
572
|
const n = chat.pendingMedia.length
|
|
573
|
+
const locale = this.resolveLocale(input)
|
|
515
574
|
const msg = n === 1
|
|
516
|
-
? '
|
|
517
|
-
:
|
|
575
|
+
? t('photo.received_one', {}, locale)
|
|
576
|
+
: t('photo.received_many', { count: n }, locale)
|
|
518
577
|
return reply(msg)
|
|
519
578
|
}
|
|
520
579
|
if (chat.pendingMedia?.length) {
|
|
@@ -535,7 +594,7 @@ export class Gateway {
|
|
|
535
594
|
|
|
536
595
|
// Hermes-like steer: while a turn is running, inject followup instead of abort+restart
|
|
537
596
|
if (this.isChatBusy(chat)) {
|
|
538
|
-
const steerText = body || (hasMedia ? '(
|
|
597
|
+
const steerText = body || (hasMedia ? '(steer: media)' : '')
|
|
539
598
|
const content = await this.buildUserContent({
|
|
540
599
|
...turnInput,
|
|
541
600
|
text: steerText,
|
|
@@ -546,7 +605,7 @@ export class Gateway {
|
|
|
546
605
|
source: { kind: 'user', plugin: PLUGIN, form: 'steer', origin: 'telegram' },
|
|
547
606
|
}))
|
|
548
607
|
chat.lastUsed = Date.now()
|
|
549
|
-
try { await reply('
|
|
608
|
+
try { await reply(t('msg.steer_added', {}, this.resolveLocale(input))) } catch {}
|
|
550
609
|
return
|
|
551
610
|
}
|
|
552
611
|
|
|
@@ -573,103 +632,194 @@ export class Gateway {
|
|
|
573
632
|
} catch (err) {
|
|
574
633
|
const msg = err instanceof Error ? err.message : String(err)
|
|
575
634
|
this.ctx.logger?.warn?.(`dsh-messenger-gateway: message: ${msg}`)
|
|
576
|
-
try { await reply(
|
|
635
|
+
try { await reply(t('msg.exception', { message: msg }, this.resolveLocale(input))) } catch {}
|
|
577
636
|
}
|
|
578
637
|
}
|
|
579
638
|
|
|
580
639
|
async handleCommand(key, text, input) {
|
|
581
|
-
|
|
582
640
|
const parts = text.split(/\s+/)
|
|
583
641
|
const cmd = parts[0].toLowerCase().split('@')[0]
|
|
584
642
|
const { reply, userId, chatId, threadId = 0, platform } = input
|
|
585
|
-
|
|
643
|
+
const locale = this.resolveLocale(input)
|
|
644
|
+
|
|
645
|
+
if (cmd === '/start') {
|
|
646
|
+
return reply(t('msg.start', {}, locale), { replyMarkup: REMOVE_REPLY_KEYBOARD })
|
|
647
|
+
}
|
|
648
|
+
|
|
586
649
|
if (cmd === '/help') {
|
|
587
650
|
return reply([
|
|
588
|
-
'📖 <b
|
|
651
|
+
'📖 <b>Messenger Gateway Help:</b>',
|
|
589
652
|
'',
|
|
590
|
-
'💬 <b
|
|
591
|
-
'• /help —
|
|
592
|
-
'• /new —
|
|
593
|
-
'• /stop —
|
|
594
|
-
'• /model —
|
|
595
|
-
'• /role [name] —
|
|
596
|
-
'• /
|
|
597
|
-
'• /
|
|
598
|
-
'• /
|
|
653
|
+
'💬 <b>Session & Chat:</b>',
|
|
654
|
+
'• /help — show this command reference',
|
|
655
|
+
'• /new — start fresh session',
|
|
656
|
+
'• /stop — interrupt current response',
|
|
657
|
+
'• /model — interactive model selector (/model list)',
|
|
658
|
+
'• /role [name] — switch persona or role (/role list)',
|
|
659
|
+
'• /bind [role] — bind persona to topic / chat',
|
|
660
|
+
'• /preset [name] — bind preset to topic / chat',
|
|
661
|
+
'• /lang [en|zh] — switch user language',
|
|
662
|
+
'• /rewind [N] — rewind last N turns',
|
|
663
|
+
'• /fork — fork session into new branch',
|
|
664
|
+
'• /export — export history to Markdown',
|
|
599
665
|
'',
|
|
600
|
-
'🛠️ <b
|
|
601
|
-
'• /skills / /tools —
|
|
602
|
-
'• /files [dir] —
|
|
603
|
-
'• /get <path> —
|
|
604
|
-
'• /remind
|
|
666
|
+
'🛠️ <b>Tools, Files & Cron:</b>',
|
|
667
|
+
'• /skills / /tools — list active tools & skills',
|
|
668
|
+
'• /files [dir] — workspace file explorer',
|
|
669
|
+
'• /get <path> — download file from workspace',
|
|
670
|
+
'• /remind <time> <text> — set reminder (/remind 10m check deploy)',
|
|
671
|
+
'• /cron <interval> <prompt> — recurring autonomous task',
|
|
605
672
|
'',
|
|
606
|
-
'⚙️ <b
|
|
607
|
-
'• /status —
|
|
608
|
-
'• /top —
|
|
609
|
-
'• /keyboard on|off —
|
|
610
|
-
'• /voice on|off|status —
|
|
611
|
-
'• /tts on|off|status —
|
|
612
|
-
'• /mute / /unmute —
|
|
673
|
+
'⚙️ <b>Settings & Stats:</b>',
|
|
674
|
+
'• /status — gateway and active model status',
|
|
675
|
+
'• /top — system resource usage (RAM, uptime)',
|
|
676
|
+
'• /keyboard on|off — quick action keyboard',
|
|
677
|
+
'• /voice on|off|status — voice replies preference',
|
|
678
|
+
'• /tts on|off|status — speech synthesis in this chat',
|
|
679
|
+
'• /mute / /unmute — mute notifications in this chat',
|
|
613
680
|
'',
|
|
614
|
-
'🔒 <b
|
|
615
|
-
'• /whoami —
|
|
616
|
-
'• /pair CODE —
|
|
617
|
-
'• /sethome [name] —
|
|
618
|
-
'• /setalert —
|
|
681
|
+
'🔒 <b>Access & Channels:</b>',
|
|
682
|
+
'• /whoami — your messenger user ID',
|
|
683
|
+
'• /pair CODE — approve pairing code',
|
|
684
|
+
'• /sethome [name] — set home notification channel',
|
|
685
|
+
'• /setalert — set alert channel',
|
|
619
686
|
].join('\n'))
|
|
620
687
|
}
|
|
688
|
+
|
|
689
|
+
if (cmd === '/lang' || cmd === '/language') {
|
|
690
|
+
const sub = parts[1]?.toLowerCase()
|
|
691
|
+
if (sub === 'en' || sub === 'zh') {
|
|
692
|
+
this.chatLocales.set(chatId, sub)
|
|
693
|
+
return reply(sub === 'zh' ? '语言已切换为中文 (zh)' : 'Language switched to English (en)')
|
|
694
|
+
}
|
|
695
|
+
const cur = this.chatLocales.get(chatId) || this.config?.defaultLocale || 'en'
|
|
696
|
+
return reply(`Current language: <b>${cur}</b>\nSwitch: <code>/lang en</code> or <code>/lang zh</code>`)
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
if (cmd === '/bind') {
|
|
700
|
+
const targetRole = parts[1]?.toLowerCase()
|
|
701
|
+
if (!targetRole || targetRole === 'list') {
|
|
702
|
+
const cur = this.personas.getPersonaForChat(chatId, threadId)
|
|
703
|
+
return reply(`${t('persona.title', {}, locale)}\nCurrent bound role: <b>${cur}</b>\nUsage: <code>/bind <role></code> (or /bind reset)`)
|
|
704
|
+
}
|
|
705
|
+
if (targetRole === 'reset' || targetRole === 'default') {
|
|
706
|
+
this.personas.set(chatId, 'default', threadId)
|
|
707
|
+
return reply(t('persona.reset', {}, locale))
|
|
708
|
+
}
|
|
709
|
+
const persona = getPersona(targetRole)
|
|
710
|
+
if (!persona) return reply(t('persona.unknown', { target: targetRole }, locale))
|
|
711
|
+
this.personas.set(chatId, targetRole, threadId)
|
|
712
|
+
return reply(t('persona.bound_topic', { kind: 'role', name: `${persona.icon} ${persona.name}` }, locale))
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
if (cmd === '/preset') {
|
|
716
|
+
const presetName = parts[1]
|
|
717
|
+
if (!presetName || presetName === 'list') {
|
|
718
|
+
const cur = this.personas.getPreset(chatId, threadId) || '(none)'
|
|
719
|
+
return reply(`🎭 <b>Presets:</b>\nCurrent topic preset: <code>${cur}</code>\nUsage: <code>/preset <name></code> or <code>/preset reset</code>`)
|
|
720
|
+
}
|
|
721
|
+
if (presetName === 'reset' || presetName === 'clear') {
|
|
722
|
+
this.personas.setPreset(chatId, threadId, null)
|
|
723
|
+
return reply('Preset cleared for this topic.')
|
|
724
|
+
}
|
|
725
|
+
this.personas.setPreset(chatId, threadId, presetName)
|
|
726
|
+
return reply(t('persona.bound_topic', { kind: 'preset', name: presetName }, locale))
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
if (cmd === '/cron') {
|
|
730
|
+
const sub = parts[1]?.toLowerCase()
|
|
731
|
+
if (sub === 'list') {
|
|
732
|
+
const list = await this.scheduler.listRecurring(chatId)
|
|
733
|
+
if (!list.length) return reply(t('cron.none', {}, locale))
|
|
734
|
+
const lines = [t('cron.list_title', {}, locale)]
|
|
735
|
+
for (const task of list) {
|
|
736
|
+
const left = formatRemaining(task.dueAt - Date.now(), locale)
|
|
737
|
+
lines.push(`• <code>${task.id}</code> (every ${formatRemaining(task.intervalMs, locale)}, next in ${left}): ${task.prompt || task.text}`)
|
|
738
|
+
}
|
|
739
|
+
lines.push('\nCancel: <code>/cron cancel ID</code>')
|
|
740
|
+
return reply(lines.join('\n'))
|
|
741
|
+
}
|
|
742
|
+
if (sub === 'cancel') {
|
|
743
|
+
const targetId = parts[2]
|
|
744
|
+
if (!targetId) return reply('Specify cron task ID: <code>/cron cancel ID</code>')
|
|
745
|
+
const ok = await this.scheduler.cancel(targetId, chatId)
|
|
746
|
+
return reply(ok ? t('cron.cancelled', { id: targetId }, locale) : `Task not found: <code>${targetId}</code>`)
|
|
747
|
+
}
|
|
748
|
+
const specArg = parts[1]
|
|
749
|
+
const promptArg = parts.slice(2).join(' ')
|
|
750
|
+
const ms = parseRelativeTime(specArg)
|
|
751
|
+
if (!ms || !promptArg) {
|
|
752
|
+
return reply('⏱️ <b>Autonomous Cron Tasks:</b>\nCreate: <code>/cron <interval> <prompt></code>\nExample: <code>/cron 1h check server logs</code>\nList: <code>/cron list</code>\nCancel: <code>/cron cancel ID</code>')
|
|
753
|
+
}
|
|
754
|
+
const task = await this.scheduler.schedule({
|
|
755
|
+
platform,
|
|
756
|
+
chatId,
|
|
757
|
+
threadId,
|
|
758
|
+
userId,
|
|
759
|
+
text: promptArg,
|
|
760
|
+
prompt: promptArg,
|
|
761
|
+
dueAt: Date.now() + ms,
|
|
762
|
+
recurring: true,
|
|
763
|
+
intervalMs: ms,
|
|
764
|
+
})
|
|
765
|
+
return reply(t('cron.scheduled', { id: task.id, schedule: specArg, prompt: promptArg }, locale))
|
|
766
|
+
}
|
|
767
|
+
|
|
621
768
|
if (cmd === '/role' || cmd === '/persona') {
|
|
622
769
|
const targetRole = parts[1]?.toLowerCase()
|
|
623
770
|
if (!targetRole || targetRole === 'list') {
|
|
624
|
-
const currentId = this.personas.
|
|
771
|
+
const currentId = this.personas.getPersonaForChat(chatId, threadId)
|
|
625
772
|
const lines = [
|
|
626
|
-
'
|
|
773
|
+
t('persona.title', {}, locale),
|
|
627
774
|
'',
|
|
628
775
|
...listPersonas().map((p) => {
|
|
629
|
-
const isCurrent = p.id === currentId ? ' (
|
|
776
|
+
const isCurrent = p.id === currentId ? ' (active)' : ''
|
|
630
777
|
return `${p.icon} <b>${p.id}</b> — ${p.name}: ${p.description}${isCurrent}`
|
|
631
778
|
}),
|
|
632
779
|
'',
|
|
633
|
-
'
|
|
780
|
+
t('persona.usage', {}, locale),
|
|
634
781
|
]
|
|
635
782
|
return reply(lines.join('\n'))
|
|
636
783
|
}
|
|
637
784
|
if (targetRole === 'reset' || targetRole === 'default') {
|
|
638
|
-
this.personas.set(chatId, 'default')
|
|
639
|
-
return reply('
|
|
785
|
+
this.personas.set(chatId, 'default', threadId)
|
|
786
|
+
return reply(t('persona.reset', {}, locale))
|
|
640
787
|
}
|
|
641
788
|
const persona = getPersona(targetRole)
|
|
642
789
|
if (!persona) {
|
|
643
|
-
return reply(
|
|
790
|
+
return reply(t('persona.unknown', { target: targetRole }, locale))
|
|
644
791
|
}
|
|
645
|
-
this.personas.set(chatId, persona.id)
|
|
646
|
-
return reply(
|
|
792
|
+
this.personas.set(chatId, persona.id, threadId)
|
|
793
|
+
return reply(t('persona.switched', { icon: persona.icon, name: persona.name, description: persona.description }, locale))
|
|
647
794
|
}
|
|
795
|
+
|
|
648
796
|
if (cmd === '/skills' || cmd === '/tools') {
|
|
649
797
|
const tools = this.ctx.get?.('tools') || this.ctx.tools
|
|
798
|
+
const toolsList = []
|
|
650
799
|
if (tools?.tools) {
|
|
651
|
-
for (const [name,
|
|
652
|
-
toolsList.push(`• <b>${name}</b>: ${
|
|
800
|
+
for (const [name, tDef] of tools.tools.entries()) {
|
|
801
|
+
toolsList.push(`• <b>${name}</b>: ${tDef.description || '(no description)'}`)
|
|
653
802
|
}
|
|
654
803
|
}
|
|
655
804
|
if (!toolsList.length) {
|
|
656
|
-
return reply('🛠️ <b
|
|
805
|
+
return reply('🛠️ <b>Agent Tools:</b>\n(no tools registered)')
|
|
657
806
|
}
|
|
658
807
|
return reply([
|
|
659
|
-
'🛠️ <b
|
|
808
|
+
'🛠️ <b>Active Tools & Skills:</b>',
|
|
660
809
|
'',
|
|
661
810
|
...toolsList,
|
|
662
811
|
].join('\n'))
|
|
663
812
|
}
|
|
813
|
+
|
|
664
814
|
if (cmd === '/export') {
|
|
665
815
|
const chat = this.chats.get(key)
|
|
666
816
|
if (!chat?.agent?.session) {
|
|
667
|
-
return reply('
|
|
817
|
+
return reply(t('msg.no_active_session', {}, locale))
|
|
668
818
|
}
|
|
669
819
|
try {
|
|
670
820
|
const { filename, buffer, messagesCount } = exportSessionToMarkdown(chat.agent.session)
|
|
671
821
|
if (!messagesCount) {
|
|
672
|
-
return reply('
|
|
822
|
+
return reply(t('export.empty', {}, locale))
|
|
673
823
|
}
|
|
674
824
|
const file = {
|
|
675
825
|
name: filename,
|
|
@@ -677,29 +827,31 @@ export class Gateway {
|
|
|
677
827
|
kind: 'document',
|
|
678
828
|
bytes: buffer,
|
|
679
829
|
}
|
|
680
|
-
return reply({ text:
|
|
830
|
+
return reply({ text: t('export.title', { count: messagesCount }, locale), files: [file] })
|
|
681
831
|
} catch (err) {
|
|
682
|
-
return reply(
|
|
832
|
+
return reply(`Export error: ${err.message}`)
|
|
683
833
|
}
|
|
684
834
|
}
|
|
835
|
+
|
|
685
836
|
if (cmd === '/rewind') {
|
|
686
837
|
const chat = this.chats.get(key)
|
|
687
838
|
if (!chat?.agent?.session) {
|
|
688
|
-
return reply('
|
|
839
|
+
return reply(t('msg.no_active_session', {}, locale))
|
|
689
840
|
}
|
|
690
841
|
const count = Number(parts[1]) || 1
|
|
691
842
|
const res = rewindSession(chat.agent.session, count)
|
|
692
843
|
if (!res.removed) {
|
|
693
|
-
return reply('
|
|
844
|
+
return reply('No turns to rewind in session history.')
|
|
694
845
|
}
|
|
695
846
|
const sessions = this.ctx.get?.('sessions') || this.ctx.sessions
|
|
696
847
|
try { await sessions?.flush(chat.agent.session) } catch {}
|
|
697
|
-
return reply(`⏪
|
|
848
|
+
return reply(`⏪ Rewound ${res.removed} messages. Remaining in context: ${res.remaining}.`)
|
|
698
849
|
}
|
|
850
|
+
|
|
699
851
|
if (cmd === '/fork') {
|
|
700
852
|
const chat = this.chats.get(key)
|
|
701
853
|
if (!chat?.agent?.session) {
|
|
702
|
-
return reply('
|
|
854
|
+
return reply(t('msg.no_active_session', {}, locale))
|
|
703
855
|
}
|
|
704
856
|
try {
|
|
705
857
|
const oldSession = chat.agent.session
|
|
@@ -713,11 +865,12 @@ export class Gateway {
|
|
|
713
865
|
try { await sessions?.flush(newChat.agent.session) } catch {}
|
|
714
866
|
}
|
|
715
867
|
this.chats.set(key, newChat)
|
|
716
|
-
return reply(`🔀
|
|
868
|
+
return reply(`🔀 Forked session!\nOld session: ${oldSession.id}\nNew session: ${newChat.agent.session.id}\nContext preserved (${oldMessages.length} messages).`)
|
|
717
869
|
} catch (err) {
|
|
718
|
-
return reply(
|
|
870
|
+
return reply(`Fork error: ${err.message}`)
|
|
719
871
|
}
|
|
720
872
|
}
|
|
873
|
+
|
|
721
874
|
if (cmd === '/files') {
|
|
722
875
|
const subPath = parts.slice(1).join(' ').trim() || '.'
|
|
723
876
|
const agentCwd = this.config.agent?.cwd || process.cwd()
|
|
@@ -725,10 +878,11 @@ export class Gateway {
|
|
|
725
878
|
if (!res.ok) return reply(`❌ ${res.error}`)
|
|
726
879
|
return reply(res.formattedText)
|
|
727
880
|
}
|
|
881
|
+
|
|
728
882
|
if (cmd === '/get') {
|
|
729
883
|
const targetRel = parts.slice(1).join(' ').trim()
|
|
730
884
|
if (!targetRel) {
|
|
731
|
-
return reply('
|
|
885
|
+
return reply('Specify file path to download: <code>/get <path></code>\nBrowse: <code>/files</code>')
|
|
732
886
|
}
|
|
733
887
|
const agentCwd = this.config.agent?.cwd || process.cwd()
|
|
734
888
|
const maxDocBytes = Number(this.config.media?.maxDocBytes) || 50 * 1024 * 1024
|
|
@@ -741,8 +895,9 @@ export class Gateway {
|
|
|
741
895
|
bytes: res.bytes,
|
|
742
896
|
dataBase64: res.bytes.toString('base64'),
|
|
743
897
|
}
|
|
744
|
-
return reply({ text: `📄
|
|
898
|
+
return reply({ text: `📄 File: <b>${res.name}</b> (${formatFileSize(res.size)})`, files: [file] })
|
|
745
899
|
}
|
|
900
|
+
|
|
746
901
|
if (cmd === '/new') {
|
|
747
902
|
const chat = this.chats.get(key)
|
|
748
903
|
if (chat) {
|
|
@@ -751,38 +906,41 @@ export class Gateway {
|
|
|
751
906
|
this.sessionToChat.delete(String(chat.agent.session.id))
|
|
752
907
|
this.chats.delete(key)
|
|
753
908
|
await chat.dispose()
|
|
754
|
-
return reply('
|
|
909
|
+
return reply('Session reset.')
|
|
755
910
|
}
|
|
756
|
-
return reply('
|
|
911
|
+
return reply(t('msg.no_active_session', {}, locale))
|
|
757
912
|
}
|
|
913
|
+
|
|
758
914
|
if (cmd === '/whoami') {
|
|
759
|
-
const lines = [
|
|
915
|
+
const lines = [`User ID: ${userId}`]
|
|
760
916
|
if (chatId) lines.push(`chatId: ${chatId}`)
|
|
761
917
|
if (threadId) lines.push(`threadId: ${threadId}`)
|
|
762
918
|
return reply(lines.join('\n'))
|
|
763
919
|
}
|
|
920
|
+
|
|
764
921
|
if (cmd === '/stop') {
|
|
765
922
|
const chat = this.chats.get(key)
|
|
766
923
|
if (chat?.turnActive || chat?.abort) {
|
|
767
924
|
try { chat.abort?.abort() } catch {}
|
|
768
925
|
releaseChatTurn(chat)
|
|
769
926
|
chat.turnActive = false
|
|
770
|
-
return reply('
|
|
927
|
+
return reply(t('msg.turn_stopped', {}, locale))
|
|
771
928
|
}
|
|
772
|
-
return reply('
|
|
929
|
+
return reply('Nothing to stop.')
|
|
773
930
|
}
|
|
931
|
+
|
|
774
932
|
if (cmd === '/status') {
|
|
775
|
-
let modelLine = '
|
|
933
|
+
let modelLine = 'model: (not set)'
|
|
776
934
|
try {
|
|
777
935
|
const sel = this.resolveAgentModel()
|
|
778
|
-
modelLine =
|
|
936
|
+
modelLine = `model: ${sel.provider}/${sel.model}`
|
|
779
937
|
} catch (e) {
|
|
780
|
-
modelLine =
|
|
938
|
+
modelLine = `model: ${e.message}`
|
|
781
939
|
}
|
|
782
940
|
const home = this.resolveHomeTarget(platform || 'telegram')
|
|
783
941
|
const homeLine = home
|
|
784
942
|
? `home: chat ${home.chatId}${home.threadId ? ` topic ${home.threadId}` : ''}`
|
|
785
|
-
: 'home:
|
|
943
|
+
: 'home: (not set)'
|
|
786
944
|
const pending = this.pairing.listPending().length
|
|
787
945
|
const up = Math.max(0, Math.round((Date.now() - this.stats.startedAt) / 1000))
|
|
788
946
|
const hh = String(Math.floor(up / 3600)).padStart(2, '0')
|
|
@@ -790,22 +948,23 @@ export class Gateway {
|
|
|
790
948
|
const ss = String(up % 60).padStart(2, '0')
|
|
791
949
|
return reply([
|
|
792
950
|
'Messenger gateway',
|
|
793
|
-
|
|
794
|
-
|
|
951
|
+
`adapters: ${[...this.adapters.keys()].join(', ') || '(none)'}`,
|
|
952
|
+
`active chats: ${this.chats.size}`,
|
|
795
953
|
modelLine,
|
|
796
954
|
homeLine,
|
|
797
955
|
`pairing pending: ${pending}`,
|
|
798
956
|
`transport: ${this.tg().transport || 'poll'}`,
|
|
799
957
|
`sessionScope: ${this.config.agent?.sessionScope || 'user'}`,
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
`polling conflict: ${this.tgAdapter?.pollingConflict ? '
|
|
958
|
+
`delivered: ${this.stats.sent}`,
|
|
959
|
+
`errors: ${this.stats.errors}`,
|
|
960
|
+
`polling conflict: ${this.tgAdapter?.pollingConflict ? 'yes' : 'no'}`,
|
|
803
961
|
`uptime: ${hh}:${mm}:${ss}`,
|
|
804
962
|
].join('\n'))
|
|
805
963
|
}
|
|
964
|
+
|
|
806
965
|
if (cmd === '/model') {
|
|
807
966
|
if (parts.length >= 3) {
|
|
808
|
-
if (!this.isUserAllowed(userId)) return reply('
|
|
967
|
+
if (!this.isUserAllowed(userId)) return reply(t('msg.not_allowed', {}, locale))
|
|
809
968
|
const provider = parts[1]
|
|
810
969
|
const model = parts.slice(2).join(' ')
|
|
811
970
|
try {
|
|
@@ -819,21 +978,21 @@ export class Gateway {
|
|
|
819
978
|
} catch (e) {
|
|
820
979
|
this.ctx.logger?.warn?.(`persist agent model: ${e.message}`)
|
|
821
980
|
}
|
|
822
|
-
return reply(
|
|
981
|
+
return reply(t('model.switched', { provider, model }, locale))
|
|
823
982
|
} catch (e) {
|
|
824
|
-
return reply(
|
|
983
|
+
return reply(`Failed to switch model: ${e.message}`)
|
|
825
984
|
}
|
|
826
985
|
}
|
|
827
986
|
try {
|
|
828
987
|
const current = this.resolveAgentModel()
|
|
829
988
|
const catalog = await listModelCatalog(this.ctx, current)
|
|
830
989
|
if (!catalog.providers.length) {
|
|
831
|
-
return reply(
|
|
990
|
+
return reply(`Current model: <code>${current.provider}/${current.model}</code>\nSwitch: <code>/model <provider> <model></code>`)
|
|
832
991
|
}
|
|
833
992
|
const kb = buildProvidersKeyboard(catalog.providers, current)
|
|
834
993
|
return reply([
|
|
835
|
-
'
|
|
836
|
-
|
|
994
|
+
t('model.title', {}, locale),
|
|
995
|
+
t('model.current', { current: `${current.provider}/${current.model}` }, locale),
|
|
837
996
|
].join('\n'), {
|
|
838
997
|
replyMarkup: kb,
|
|
839
998
|
})
|
|
@@ -841,38 +1000,42 @@ export class Gateway {
|
|
|
841
1000
|
return reply(e.message)
|
|
842
1001
|
}
|
|
843
1002
|
}
|
|
1003
|
+
|
|
844
1004
|
if (cmd === '/pair') {
|
|
845
|
-
if (!this.isUserAllowed(userId)) return reply('
|
|
1005
|
+
if (!this.isUserAllowed(userId)) return reply('Only users in allowlist can approve /pair.')
|
|
846
1006
|
const code = parts[1]
|
|
847
|
-
if (!code) return reply('
|
|
1007
|
+
if (!code) return reply('Usage: /pair CODE')
|
|
848
1008
|
const res = this.pairing.approveCode(code, userId)
|
|
849
|
-
if (!res.ok) return reply(
|
|
1009
|
+
if (!res.ok) return reply(`Failed: ${res.error}`)
|
|
850
1010
|
const merged = this.effectiveAllowedIds()
|
|
851
1011
|
for (const a of this.adapterList) a.setAllowedUserIds?.(merged)
|
|
852
1012
|
try { await this.hooks?.persistAllowedUserIds?.(merged) } catch (e) {
|
|
853
1013
|
this.ctx.logger?.warn?.(`persist allowlist: ${e.message}`)
|
|
854
1014
|
}
|
|
855
|
-
return reply(
|
|
1015
|
+
return reply(`Approved user ID ${res.userId}${res.username ? ` (@${res.username})` : ''}.`)
|
|
856
1016
|
}
|
|
1017
|
+
|
|
857
1018
|
if (cmd === '/sethome') {
|
|
858
|
-
if (!this.isUserAllowed(userId)) return reply('
|
|
1019
|
+
if (!this.isUserAllowed(userId)) return reply(t('msg.not_allowed', {}, locale))
|
|
859
1020
|
const name = normalizeHomeName(parts[1] || 'default') || 'default'
|
|
860
1021
|
try {
|
|
861
1022
|
const nextTg = upsertHome(this.tg(), { name, chatId, threadId })
|
|
862
1023
|
await this.hooks?.persistHomes?.(nextTg)
|
|
863
1024
|
this.config.telegram = nextTg
|
|
864
|
-
return reply(`Home
|
|
1025
|
+
return reply(`Home "${name}": chat ${chatId}${threadId ? ` topic ${threadId}` : ''}`)
|
|
865
1026
|
} catch (e) {
|
|
866
|
-
return reply(
|
|
1027
|
+
return reply(`Failed to save home: ${e.message}`)
|
|
867
1028
|
}
|
|
868
1029
|
}
|
|
1030
|
+
|
|
869
1031
|
if (cmd === '/home') {
|
|
870
1032
|
const homes = listHomes(this.tg())
|
|
871
|
-
if (!homes.length) return reply('Home
|
|
1033
|
+
if (!homes.length) return reply('Home is not set. /sethome or /sethome <name>')
|
|
872
1034
|
return reply(['Homes:', ...homes.map((h) => `• ${h.name}: chat ${h.chatId}${h.threadId ? ` topic ${h.threadId}` : ''}`)].join('\n'))
|
|
873
1035
|
}
|
|
1036
|
+
|
|
874
1037
|
if (cmd === '/setalert') {
|
|
875
|
-
if (!this.isUserAllowed(userId)) return reply('
|
|
1038
|
+
if (!this.isUserAllowed(userId)) return reply(t('msg.not_allowed', {}, locale))
|
|
876
1039
|
const nextTg = {
|
|
877
1040
|
...this.tg(),
|
|
878
1041
|
alerts: {
|
|
@@ -884,62 +1047,64 @@ export class Gateway {
|
|
|
884
1047
|
}
|
|
885
1048
|
this.config.telegram = nextTg
|
|
886
1049
|
try { await this.hooks?.persistHomes?.(nextTg) } catch {}
|
|
887
|
-
return reply(`🔔
|
|
1050
|
+
return reply(`🔔 This chat assigned as alert channel (chat: ${chatId}${threadId ? `, topic: ${threadId}` : ''}).`)
|
|
888
1051
|
}
|
|
1052
|
+
|
|
889
1053
|
if (cmd === '/alert') {
|
|
890
1054
|
const sub = parts[1]?.toLowerCase()
|
|
891
1055
|
if (sub === 'test') {
|
|
892
1056
|
const target = resolveAlertTarget(this)
|
|
893
|
-
if (!target) return reply('
|
|
894
|
-
await this.sendAlert('status', { title: '
|
|
895
|
-
return reply('
|
|
1057
|
+
if (!target) return reply('Alert channel not configured. Configure: /setalert')
|
|
1058
|
+
await this.sendAlert('status', { title: 'Test Alert', details: `Sent by user ID ${userId}` })
|
|
1059
|
+
return reply('Test alert sent to alert channel.')
|
|
896
1060
|
}
|
|
897
1061
|
const target = resolveAlertTarget(this)
|
|
898
1062
|
const alertsCfg = this.tg().alerts || {}
|
|
899
1063
|
return reply([
|
|
900
|
-
'🔔 <b
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
1064
|
+
'🔔 <b>Alert Channel:</b>',
|
|
1065
|
+
`Status: ${alertsCfg.enabled ? 'enabled' : 'disabled'}`,
|
|
1066
|
+
`Chat: ${target ? `${target.chatId}${target.threadId ? ` (topic: ${target.threadId})` : ''}` : '(not assigned)'}`,
|
|
1067
|
+
`Events: ${(alertsCfg.events || ['error', 'pairing']).join(', ')}`,
|
|
904
1068
|
'',
|
|
905
|
-
'
|
|
906
|
-
'/setalert —
|
|
907
|
-
'/alert test —
|
|
1069
|
+
'Commands:',
|
|
1070
|
+
'/setalert — assign current chat as alert channel',
|
|
1071
|
+
'/alert test — send test alert',
|
|
908
1072
|
].join('\n'))
|
|
909
1073
|
}
|
|
1074
|
+
|
|
910
1075
|
if (cmd === '/remind') {
|
|
911
1076
|
const sub = parts[1]?.toLowerCase()
|
|
912
1077
|
if (sub === 'list') {
|
|
913
1078
|
const active = await this.scheduler.list(chatId)
|
|
914
|
-
if (!active.length) return reply('
|
|
1079
|
+
if (!active.length) return reply('No active reminders for this chat.')
|
|
915
1080
|
const lines = [
|
|
916
|
-
'⏰ <b
|
|
1081
|
+
'⏰ <b>Active Reminders:</b>',
|
|
917
1082
|
'',
|
|
918
|
-
...active.map((
|
|
919
|
-
const left = formatRemaining(
|
|
920
|
-
return `• <code>${
|
|
1083
|
+
...active.map((tItem) => {
|
|
1084
|
+
const left = formatRemaining(tItem.dueAt - Date.now(), locale)
|
|
1085
|
+
return `• <code>${tItem.id}</code> (in ${left}): ${tItem.text}`
|
|
921
1086
|
}),
|
|
922
1087
|
'',
|
|
923
|
-
'
|
|
1088
|
+
'Cancel: <code>/remind cancel ID</code>',
|
|
924
1089
|
]
|
|
925
1090
|
return reply(lines.join('\n'))
|
|
926
1091
|
}
|
|
927
1092
|
if (sub === 'cancel') {
|
|
928
1093
|
const targetId = parts[2]?.trim()
|
|
929
|
-
if (!targetId) return reply('
|
|
1094
|
+
if (!targetId) return reply('Specify reminder ID: <code>/remind cancel ID</code>')
|
|
930
1095
|
const ok = await this.scheduler.cancel(targetId, chatId)
|
|
931
|
-
return reply(ok ? `✅
|
|
1096
|
+
return reply(ok ? `✅ Reminder <code>${targetId}</code> cancelled.` : `❌ Reminder <code>${targetId}</code> not found.`)
|
|
932
1097
|
}
|
|
933
1098
|
const timeArg = parts[1]
|
|
934
1099
|
const textArg = parts.slice(2).join(' ').trim()
|
|
935
1100
|
const delayMs = parseRelativeTime(timeArg)
|
|
936
1101
|
if (!delayMs || !textArg) {
|
|
937
1102
|
return reply([
|
|
938
|
-
'⏰ <b
|
|
939
|
-
'
|
|
940
|
-
'
|
|
941
|
-
'
|
|
942
|
-
'
|
|
1103
|
+
'⏰ <b>Reminders:</b>',
|
|
1104
|
+
'Create: <code>/remind <time> <text></code>',
|
|
1105
|
+
'Examples: <code>/remind 10m Call colleague</code>, <code>/remind 2h Check deploy</code>',
|
|
1106
|
+
'List: <code>/remind list</code>',
|
|
1107
|
+
'Cancel: <code>/remind cancel ID</code>',
|
|
943
1108
|
].join('\n'))
|
|
944
1109
|
}
|
|
945
1110
|
const dueAt = Date.now() + delayMs
|
|
@@ -951,9 +1116,10 @@ export class Gateway {
|
|
|
951
1116
|
text: textArg,
|
|
952
1117
|
dueAt,
|
|
953
1118
|
})
|
|
954
|
-
const left = formatRemaining(delayMs)
|
|
955
|
-
return reply(
|
|
1119
|
+
const left = formatRemaining(delayMs, locale)
|
|
1120
|
+
return reply(t('remind.scheduled', { time: new Date(dueAt).toLocaleTimeString(), duration: left, text: textArg }, locale))
|
|
956
1121
|
}
|
|
1122
|
+
|
|
957
1123
|
if (cmd === '/voice') {
|
|
958
1124
|
const sub = String(parts[1] || 'status').toLowerCase()
|
|
959
1125
|
if (sub === 'summary') {
|
|
@@ -961,44 +1127,46 @@ export class Gateway {
|
|
|
961
1127
|
if (val === 'on' || val === 'off') {
|
|
962
1128
|
if (!this.config.tts) this.config.tts = {}
|
|
963
1129
|
this.config.tts.voiceSummary = val === 'on'
|
|
964
|
-
return reply(
|
|
1130
|
+
return reply(`Voice summary (TL;DR): ${val === 'on' ? 'enabled' : 'disabled'}`)
|
|
965
1131
|
}
|
|
966
1132
|
const state = this.config.tts?.voiceSummary ? 'on' : 'off'
|
|
967
|
-
return reply(
|
|
1133
|
+
return reply(`Voice summary (TL;DR): ${state}\nToggle: <code>/voice summary on|off</code>`)
|
|
968
1134
|
}
|
|
969
1135
|
if (sub === 'on' || sub === 'off') {
|
|
970
1136
|
this.voicePrefs.set(userId, sub === 'on')
|
|
971
|
-
return reply(sub === 'on' ? '
|
|
1137
|
+
return reply(sub === 'on' ? 'Voice replies: on (for you)' : 'Voice replies: off (for you)')
|
|
972
1138
|
}
|
|
973
1139
|
const pref = this.voicePrefs.get(userId)
|
|
974
1140
|
const mode = this.tg().voiceMode || 'mirror'
|
|
975
|
-
const prefLine = pref === null ? '
|
|
1141
|
+
const prefLine = pref === null ? 'not set (/voice on|off)' : (pref ? 'on' : 'off')
|
|
976
1142
|
const summaryState = this.config.tts?.voiceSummary ? 'on' : 'off'
|
|
977
|
-
return reply(`voiceMode=${mode}\
|
|
1143
|
+
return reply(`voiceMode=${mode}\nyour /voice: ${prefLine}\nglobal tts: ${this.config.tts?.enabled ? 'on' : 'off'}\nvoice summary: ${summaryState}`)
|
|
978
1144
|
}
|
|
1145
|
+
|
|
979
1146
|
if (cmd === '/topic') {
|
|
980
1147
|
const topicName = parts.slice(1).join(' ').trim()
|
|
981
1148
|
if (!topicName) {
|
|
982
|
-
return reply('
|
|
1149
|
+
return reply('Usage: <code>/topic <name></code>\nCreates a new topic in supergroup with an isolated session.')
|
|
983
1150
|
}
|
|
984
1151
|
const tgAdapter = this.getAdapter('telegram')
|
|
985
1152
|
if (!tgAdapter?.createForumTopic) {
|
|
986
|
-
return reply('
|
|
1153
|
+
return reply('Topic creation is available only in Telegram.')
|
|
987
1154
|
}
|
|
988
1155
|
try {
|
|
989
1156
|
const res = await tgAdapter.createForumTopic(chatId, topicName)
|
|
990
1157
|
const newThreadId = res?.message_thread_id
|
|
991
|
-
await reply(`🎯
|
|
1158
|
+
await reply(`🎯 Created new topic <b>«${topicName}»</b> (ID: <code>${newThreadId}</code>).\nSwitch to the topic to continue working!`)
|
|
992
1159
|
if (newThreadId) {
|
|
993
1160
|
await tgAdapter.sendTo(chatId, {
|
|
994
|
-
text: `👋
|
|
1161
|
+
text: `👋 Hello! This is an isolated session for task <b>«${topicName}»</b>.\nHow can I help?`,
|
|
995
1162
|
}, { threadId: newThreadId })
|
|
996
1163
|
}
|
|
997
1164
|
return
|
|
998
1165
|
} catch (err) {
|
|
999
|
-
return reply(
|
|
1166
|
+
return reply(`Failed to create topic: ${err.message}\n(Ensure the bot is group administrator with Manage Topics permission)`)
|
|
1000
1167
|
}
|
|
1001
1168
|
}
|
|
1169
|
+
|
|
1002
1170
|
if (cmd === '/top') {
|
|
1003
1171
|
const mem = process.memoryUsage()
|
|
1004
1172
|
const rssMb = (mem.rss / 1024 / 1024).toFixed(1)
|
|
@@ -1009,7 +1177,7 @@ export class Gateway {
|
|
|
1009
1177
|
const ss = String(sec % 60).padStart(2, '0')
|
|
1010
1178
|
let activeReminders = 0
|
|
1011
1179
|
try { activeReminders = (await this.scheduler.list()).length } catch {}
|
|
1012
|
-
let currentModel = '
|
|
1180
|
+
let currentModel = 'not set'
|
|
1013
1181
|
try {
|
|
1014
1182
|
const m = this.resolveAgentModel()
|
|
1015
1183
|
currentModel = `${m.provider}/${m.model}`
|
|
@@ -1017,60 +1185,65 @@ export class Gateway {
|
|
|
1017
1185
|
|
|
1018
1186
|
return reply([
|
|
1019
1187
|
'📊 <b>DSH System & Resources:</b>',
|
|
1020
|
-
`• <b
|
|
1188
|
+
`• <b>Memory (RSS):</b> ${rssMb} MB`,
|
|
1021
1189
|
`• <b>Heap:</b> ${heapMb} MB`,
|
|
1022
|
-
`• <b
|
|
1023
|
-
`• <b
|
|
1024
|
-
`• <b
|
|
1025
|
-
`• <b
|
|
1026
|
-
`• <b
|
|
1027
|
-
`• <b
|
|
1190
|
+
`• <b>Uptime:</b> ${hh}:${mm}:${ss}`,
|
|
1191
|
+
`• <b>Active chats:</b> ${this.chats.size}`,
|
|
1192
|
+
`• <b>Queued reminders:</b> ${activeReminders}`,
|
|
1193
|
+
`• <b>Active model:</b> <code>${currentModel}</code>`,
|
|
1194
|
+
`• <b>Messages sent:</b> ${this.stats.sent}`,
|
|
1195
|
+
`• <b>Errors:</b> ${this.stats.errors}`,
|
|
1028
1196
|
].join('\n'))
|
|
1029
1197
|
}
|
|
1198
|
+
|
|
1030
1199
|
if (cmd === '/keyboard') {
|
|
1031
1200
|
const sub = String(parts[1] || '').toLowerCase()
|
|
1032
1201
|
const tgAdapter = this.getAdapter('telegram')
|
|
1033
1202
|
if (sub === 'on') {
|
|
1034
1203
|
if (tgAdapter) tgAdapter.quickActions = true
|
|
1035
|
-
return reply('
|
|
1204
|
+
return reply('Quick action keyboard enabled.', {
|
|
1036
1205
|
replyMarkup: buildQuickActionsKeyboard(),
|
|
1037
1206
|
})
|
|
1038
1207
|
}
|
|
1039
1208
|
if (sub === 'off') {
|
|
1040
1209
|
if (tgAdapter) tgAdapter.quickActions = false
|
|
1041
|
-
return reply('
|
|
1210
|
+
return reply('Quick action keyboard disabled.', {
|
|
1042
1211
|
replyMarkup: REMOVE_REPLY_KEYBOARD,
|
|
1043
1212
|
})
|
|
1044
1213
|
}
|
|
1045
|
-
const curState = tgAdapter?.quickActions ? '
|
|
1214
|
+
const curState = tgAdapter?.quickActions ? 'enabled' : 'disabled'
|
|
1046
1215
|
return reply([
|
|
1047
|
-
'⌨️ <b
|
|
1048
|
-
|
|
1216
|
+
'⌨️ <b>Quick Action Keyboard:</b>',
|
|
1217
|
+
`Current state: <b>${curState}</b>`,
|
|
1049
1218
|
'',
|
|
1050
|
-
'
|
|
1051
|
-
'<code>/keyboard on</code> —
|
|
1052
|
-
'<code>/keyboard off</code> —
|
|
1219
|
+
'Commands:',
|
|
1220
|
+
'<code>/keyboard on</code> — show buttons',
|
|
1221
|
+
'<code>/keyboard off</code> — hide buttons',
|
|
1053
1222
|
].join('\n'))
|
|
1054
1223
|
}
|
|
1224
|
+
|
|
1055
1225
|
if (cmd === '/tts') {
|
|
1056
1226
|
const sub = String(parts[1] || 'status').toLowerCase()
|
|
1057
1227
|
if (sub === 'on' || sub === 'off') {
|
|
1058
1228
|
this.chatTts.set(chatId, sub === 'on')
|
|
1059
|
-
return reply(sub === 'on' ? '
|
|
1229
|
+
return reply(sub === 'on' ? 'Speech in this chat: on' : 'Speech in this chat: off')
|
|
1060
1230
|
}
|
|
1061
1231
|
const cur = this.chatTts.get(chatId)
|
|
1062
|
-
const line = cur === null ? '
|
|
1063
|
-
return reply(
|
|
1232
|
+
const line = cur === null ? 'not set (/tts on|off)' : (cur ? 'on' : 'off')
|
|
1233
|
+
return reply(`Speech in this chat: ${line}\nglobal tts: ${this.config.tts?.enabled ? 'on' : 'off'}`)
|
|
1064
1234
|
}
|
|
1235
|
+
|
|
1065
1236
|
if (cmd === '/mute') {
|
|
1066
1237
|
this.setMuted(chatId, true)
|
|
1067
|
-
return reply('
|
|
1238
|
+
return reply(t('msg.muted_on', {}, locale))
|
|
1068
1239
|
}
|
|
1240
|
+
|
|
1069
1241
|
if (cmd === '/unmute') {
|
|
1070
1242
|
this.setMuted(chatId, false)
|
|
1071
|
-
return reply('
|
|
1243
|
+
return reply(t('msg.muted_off', {}, locale))
|
|
1072
1244
|
}
|
|
1073
|
-
|
|
1245
|
+
|
|
1246
|
+
return reply(t('msg.unknown_command', { cmd }, locale))
|
|
1074
1247
|
}
|
|
1075
1248
|
|
|
1076
1249
|
collectDynamicSkills() {
|
|
@@ -1130,7 +1303,7 @@ export class Gateway {
|
|
|
1130
1303
|
this.sessionToThread.set(sessionId, { chatId: forumChatId, threadId })
|
|
1131
1304
|
this.threadToSession.set(threadKey, sessionId)
|
|
1132
1305
|
|
|
1133
|
-
const text =
|
|
1306
|
+
const text = t('mirror.created', { sessionId, title }, 'en')
|
|
1134
1307
|
await tgAdapter.sendTo(forumChatId, { text }, { threadId })
|
|
1135
1308
|
this.ctx.logger?.info?.(`Mirrored session ${sessionId} to Telegram forum topic ${threadId} in ${forumChatId}`)
|
|
1136
1309
|
} catch (err) {
|
|
@@ -1197,7 +1370,7 @@ export class Gateway {
|
|
|
1197
1370
|
if (provider && model) return { provider, model }
|
|
1198
1371
|
const selection = this.ctx.get('agentDefaultModel')?.currentSelection?.()
|
|
1199
1372
|
if (!selection?.provider || !selection?.model) {
|
|
1200
|
-
throw new Error('
|
|
1373
|
+
throw new Error('Please select a model in Settings -> Models (or set agent.provider/model in profile)')
|
|
1201
1374
|
}
|
|
1202
1375
|
return { provider: provider || selection.provider, model: model || selection.model }
|
|
1203
1376
|
}
|
|
@@ -1224,6 +1397,7 @@ export class Gateway {
|
|
|
1224
1397
|
const chat = {
|
|
1225
1398
|
key, agent: handle.agent, dispose: handle.dispose, busy: Promise.resolve(),
|
|
1226
1399
|
lastUsed: Date.now(), abort: undefined, pendingMedia: [], turnActive: false,
|
|
1400
|
+
sessionAllowlist: new Set(),
|
|
1227
1401
|
target: input ? { platform: input.platform, chatId: input.chatId, threadId: input.threadId || 0 } : undefined,
|
|
1228
1402
|
}
|
|
1229
1403
|
this.sessionToChat.set(String(handle.agent.session.id), key)
|
|
@@ -1240,12 +1414,33 @@ export class Gateway {
|
|
|
1240
1414
|
const chat = this.chats.get(chatKeyValue)
|
|
1241
1415
|
if (!chat?.target) return next()
|
|
1242
1416
|
const tool = req.toolName || 'tool'
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1417
|
+
if (chat.sessionAllowlist?.has(tool)) {
|
|
1418
|
+
return 'allowed-once'
|
|
1419
|
+
}
|
|
1420
|
+
const locale = this.resolveLocale(chat.target)
|
|
1421
|
+
const reason = req.reason ? `\n<i>${req.reason}</i>` : ''
|
|
1422
|
+
let detail = ''
|
|
1423
|
+
if (req.input && typeof req.input === 'object') {
|
|
1424
|
+
try {
|
|
1425
|
+
const jsonStr = JSON.stringify(req.input, null, 2)
|
|
1426
|
+
detail = `\n<pre><code>${jsonStr.slice(0, 500)}</code></pre>`
|
|
1427
|
+
} catch {}
|
|
1428
|
+
}
|
|
1429
|
+
const text = t('ask.confirm_title', { tool, reason: reason + detail }, locale)
|
|
1430
|
+
const buttons = [
|
|
1431
|
+
[
|
|
1432
|
+
{ id: 'allow_once', text: t('ask.allow_once', {}, locale) },
|
|
1433
|
+
{ id: 'allow_session', text: t('ask.allow_session', {}, locale) },
|
|
1434
|
+
{ id: 'deny', text: t('ask.deny', {}, locale) },
|
|
1435
|
+
],
|
|
1436
|
+
]
|
|
1437
|
+
const result = await this.messengerAsk(chat.target, { text, buttons }, 300_000)
|
|
1438
|
+
if (result?.buttonId === 'allow_once' || result?.buttonId === 'allow') return 'allowed-once'
|
|
1439
|
+
if (result?.buttonId === 'allow_session') {
|
|
1440
|
+
if (!chat.sessionAllowlist) chat.sessionAllowlist = new Set()
|
|
1441
|
+
chat.sessionAllowlist.add(tool)
|
|
1442
|
+
return 'allowed-once'
|
|
1443
|
+
}
|
|
1249
1444
|
if (result?.buttonId === 'deny') return 'rejected'
|
|
1250
1445
|
return next()
|
|
1251
1446
|
} catch {
|
|
@@ -1257,13 +1452,13 @@ export class Gateway {
|
|
|
1257
1452
|
const { text, attachments = [], replyText, steer, personaOverride } = input
|
|
1258
1453
|
const parts = []
|
|
1259
1454
|
parts.push(String(this.config.agent?.instructionPrefix || MESSENGER_RELAY_INSTRUCTION))
|
|
1260
|
-
const activePersonaId = personaOverride || this.personas.
|
|
1455
|
+
const activePersonaId = personaOverride || this.personas.getPersonaForChat(input.chatId, input.threadId)
|
|
1261
1456
|
const activePersona = getPersona(activePersonaId)
|
|
1262
1457
|
if (activePersona?.instruction) {
|
|
1263
1458
|
parts.push(`[Persona: ${activePersona.name} (${activePersona.icon})]\n${activePersona.instruction}`)
|
|
1264
1459
|
}
|
|
1265
|
-
if (steer) parts.push('[Steer /
|
|
1266
|
-
if (replyText?.trim()) parts.push(`[
|
|
1460
|
+
if (steer) parts.push('[Steer / addition to current turn: combine with previous instruction, do not restart from scratch]')
|
|
1461
|
+
if (replyText?.trim()) parts.push(`[Replying to message: ${replyText.trim()}]`)
|
|
1267
1462
|
const blocks = []
|
|
1268
1463
|
for (const att of attachments) {
|
|
1269
1464
|
if (att.kind === 'photo' || (att.kind === 'sticker' && att.mime?.startsWith('image/'))) {
|
|
@@ -1273,22 +1468,22 @@ export class Gateway {
|
|
|
1273
1468
|
maxBytes: Number(this.config.media?.maxImageBytes) || 20 * 1024 * 1024,
|
|
1274
1469
|
})
|
|
1275
1470
|
blocks.push({ type: 'image', attachment: ref })
|
|
1276
|
-
if (att.kind === 'sticker' && att.emoji) parts.push(`[
|
|
1471
|
+
if (att.kind === 'sticker' && att.emoji) parts.push(`[Sticker ${att.emoji}]`)
|
|
1277
1472
|
} catch (err) {
|
|
1278
1473
|
if (signal?.aborted) throw err
|
|
1279
1474
|
const msg = err instanceof Error ? err.message : String(err)
|
|
1280
|
-
parts.push(`[
|
|
1475
|
+
parts.push(`[Failed to attach image: ${msg}]`)
|
|
1281
1476
|
}
|
|
1282
1477
|
} else if (att.kind === 'voice' || att.kind === 'audio') {
|
|
1283
1478
|
try {
|
|
1284
1479
|
const bytes = new Uint8Array(await readFile(att.path))
|
|
1285
1480
|
const transcript = await transcribeVoice(this.baseUrl(), bytes, att.mime || 'audio/ogg', 'message', signal)
|
|
1286
|
-
parts.push(transcript ? `[
|
|
1481
|
+
parts.push(transcript ? `[Voice message transcript: ${transcript}]` : '[Voice message (unrecognized)]')
|
|
1287
1482
|
} catch (err) {
|
|
1288
1483
|
if (signal?.aborted) throw err
|
|
1289
1484
|
const msg = err instanceof Error ? err.message : String(err)
|
|
1290
1485
|
this.ctx.logger?.warn?.(`voice: ${msg}`)
|
|
1291
|
-
parts.push(`[
|
|
1486
|
+
parts.push(`[Voice message (dsh-voice unavailable: ${msg})]`)
|
|
1292
1487
|
}
|
|
1293
1488
|
} else if (att.kind === 'document' || att.kind === 'video' || att.kind === 'animation' || att.kind === 'sticker') {
|
|
1294
1489
|
let parsed = null
|
|
@@ -1300,7 +1495,7 @@ export class Gateway {
|
|
|
1300
1495
|
}
|
|
1301
1496
|
parts.push(formatInboundDocument(att, parsed))
|
|
1302
1497
|
} else {
|
|
1303
|
-
parts.push(`[
|
|
1498
|
+
parts.push(`[File: ${att.path}${att.name ? ` (${att.name})` : ''}]`)
|
|
1304
1499
|
}
|
|
1305
1500
|
}
|
|
1306
1501
|
const photoHint = photoOnlyHint(attachments, text)
|
|
@@ -1310,7 +1505,7 @@ export class Gateway {
|
|
|
1310
1505
|
if (text?.trim()) parts.push(text.trim())
|
|
1311
1506
|
const textBlock = parts.filter(Boolean).join('\n\n')
|
|
1312
1507
|
if (textBlock) blocks.unshift({ type: 'text', text: textBlock })
|
|
1313
|
-
if (!blocks.length) blocks.push({ type: 'text', text: '(
|
|
1508
|
+
if (!blocks.length) blocks.push({ type: 'text', text: '(empty message)' })
|
|
1314
1509
|
return blocks
|
|
1315
1510
|
}
|
|
1316
1511
|
|
|
@@ -1371,8 +1566,9 @@ export class Gateway {
|
|
|
1371
1566
|
if (signal.aborted) {
|
|
1372
1567
|
if (progress) try { await progress.remove() } catch {}
|
|
1373
1568
|
if (typeof react === 'function') react('').catch?.(() => {})
|
|
1374
|
-
|
|
1375
|
-
|
|
1569
|
+
const stoppedMsg = t('msg.turn_stopped', {}, this.resolveLocale(input))
|
|
1570
|
+
if (stream) try { await stream.finalize(stoppedMsg) } catch {}
|
|
1571
|
+
else return reply(stoppedMsg)
|
|
1376
1572
|
return
|
|
1377
1573
|
}
|
|
1378
1574
|
const sessions = this.ctx.get?.('sessions') || this.ctx.sessions
|
|
@@ -1387,7 +1583,7 @@ export class Gateway {
|
|
|
1387
1583
|
const answer = processed.text
|
|
1388
1584
|
if (collector.reason?.kind === 'error') {
|
|
1389
1585
|
const err = collector.reason.error
|
|
1390
|
-
const msg =
|
|
1586
|
+
const msg = t('msg.agent_error', { code: err?.code || 'error', message: err?.message || 'unknown' }, this.resolveLocale(input))
|
|
1391
1587
|
this.sendAlert('error', {
|
|
1392
1588
|
code: err?.code || 'AGENT_ERROR',
|
|
1393
1589
|
message: err?.message || 'unknown',
|
|
@@ -1402,15 +1598,16 @@ export class Gateway {
|
|
|
1402
1598
|
const files = await buildOutboundFiles(this.ctx, this.baseUrl(), collector, { signal, logger: this.ctx.logger })
|
|
1403
1599
|
const allFiles = [...files, ...(processed.files || [])]
|
|
1404
1600
|
if (!answer && !allFiles.length) {
|
|
1405
|
-
|
|
1406
|
-
|
|
1601
|
+
const noResp = t('msg.no_response', {}, this.resolveLocale(input))
|
|
1602
|
+
if (stream) { await scheduler?.flush(); await stream.finalize(noResp) }
|
|
1603
|
+
else await reply(noResp)
|
|
1407
1604
|
return
|
|
1408
1605
|
}
|
|
1409
1606
|
const maxLen = Number(this.config.agent?.maxMessageLength) || 4000
|
|
1410
1607
|
const chunks = answer ? splitText(answer, maxLen) : ['']
|
|
1411
1608
|
if (stream) {
|
|
1412
1609
|
await scheduler?.flush()
|
|
1413
|
-
await stream.finalize(chunks[0] || '(
|
|
1610
|
+
await stream.finalize(chunks[0] || t('msg.no_response', {}, this.resolveLocale(input)))
|
|
1414
1611
|
for (let i = 1; i < chunks.length; i++) await reply({ text: chunks[i] })
|
|
1415
1612
|
if (allFiles.length) await reply({ files: allFiles })
|
|
1416
1613
|
} else {
|
|
@@ -1449,8 +1646,9 @@ export class Gateway {
|
|
|
1449
1646
|
threadId: input.threadId,
|
|
1450
1647
|
}).catch(() => {})
|
|
1451
1648
|
try {
|
|
1452
|
-
|
|
1453
|
-
|
|
1649
|
+
const excMsg = t('msg.exception', { message: err.message }, this.resolveLocale(input))
|
|
1650
|
+
if (stream) await stream.finalize(excMsg)
|
|
1651
|
+
else await reply(excMsg)
|
|
1454
1652
|
} catch {}
|
|
1455
1653
|
}
|
|
1456
1654
|
} finally {
|