@goodandready/dsh-messenger-gateway 0.3.2 → 0.3.9
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 +33 -2
- package/README.ru.md +125 -0
- package/README.zh.md +76 -0
- package/lib/adapters/discord.js +129 -4
- package/lib/adapters/index.js +17 -1
- package/lib/adapters/slack.js +91 -0
- package/lib/adapters/telegram.js +35 -2
- package/lib/alerts.js +64 -0
- package/lib/artifacts.js +118 -0
- package/lib/ask.js +89 -0
- package/lib/client.js +12 -0
- package/lib/commands.js +15 -1
- package/lib/config.js +21 -1
- package/lib/documents.js +145 -3
- package/lib/file-manager.js +162 -0
- package/lib/gateway.js +589 -17
- package/lib/index.js +65 -8
- package/lib/messenger-api.js +19 -1
- package/lib/models.js +106 -0
- package/lib/personas.js +98 -0
- package/lib/scheduler.js +135 -0
- package/lib/session-ops.js +95 -0
- package/lib/tts.js +20 -2
- package/package.json +1 -1
package/lib/gateway.js
CHANGED
|
@@ -8,7 +8,8 @@ import { SessionId } from '@deepseek-ai/dsh-session'
|
|
|
8
8
|
import createAdapters from './adapters/index.js'
|
|
9
9
|
import { transcribeVoice, speakText } from './integrations.js'
|
|
10
10
|
import { attachInboundPhoto, photoOnlyHint } from './photos.js'
|
|
11
|
-
import { formatInboundDocument, documentOnlyHint } from './documents.js'
|
|
11
|
+
import { formatInboundDocument, documentOnlyHint, parseDocument } from './documents.js'
|
|
12
|
+
import { listFiles, getFileForDownload, formatFileSize } from './file-manager.js'
|
|
12
13
|
import { collectAssistantParts, buildOutboundFiles, stripImageUrls } from './outbound.js'
|
|
13
14
|
import { assistantText, splitText, stripReasoningPreamble, MESSENGER_RELAY_INSTRUCTION } from './text.js'
|
|
14
15
|
import { chatKey, sessionKey } from './topics.js'
|
|
@@ -16,9 +17,16 @@ import { listHomes, resolveNamedHome, upsertHome, normalizeHomeName } from './ho
|
|
|
16
17
|
import { createVoicePrefs, shouldSpeakReply } from './voice-prefs.js'
|
|
17
18
|
import { prepareTtsText, toTelegramVoiceFile } from './tts.js'
|
|
18
19
|
import {
|
|
19
|
-
makeAskToken, buildInlineKeyboard, indexCallbacks, releaseCallbacks,
|
|
20
|
-
parseCallbackData, targetMatchesAsk, rejectPendingAsk, REMOVE_KEYBOARD,
|
|
20
|
+
makeAskToken, buildInlineKeyboard, buildMultiSelectKeyboard, indexCallbacks, releaseCallbacks,
|
|
21
|
+
parseCallbackData, parseAskCallback, targetMatchesAsk, rejectPendingAsk, REMOVE_KEYBOARD,
|
|
21
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'
|
|
22
30
|
import { createPairingStore } from './pairing.js'
|
|
23
31
|
import {
|
|
24
32
|
extractTextDelta, extractToolName, buildStreamPreview, formatProgressLine,
|
|
@@ -64,6 +72,16 @@ export class Gateway {
|
|
|
64
72
|
this.voicePrefs = createVoicePrefs(join(home, 'messenger-gateway', 'voice-prefs.json'))
|
|
65
73
|
this.chatTts = createVoicePrefs(join(home, 'messenger-gateway', 'chat-tts.json'))
|
|
66
74
|
this.muted = createVoicePrefs(join(home, 'messenger-gateway', 'muted.json'))
|
|
75
|
+
this.personas = createPersonaStore(join(home, 'messenger-gateway', 'personas.json'))
|
|
76
|
+
this.scheduler = createScheduler(join(home, 'messenger-gateway', 'scheduled.json'), async (task) => {
|
|
77
|
+
const target = {
|
|
78
|
+
platform: task.platform || 'telegram',
|
|
79
|
+
chatId: task.chatId,
|
|
80
|
+
threadId: task.threadId || 0,
|
|
81
|
+
}
|
|
82
|
+
const text = `⏰ <b>[Напоминание]</b>\n${task.text}`
|
|
83
|
+
await this.sendToMessenger(target, { text })
|
|
84
|
+
})
|
|
67
85
|
this.stats = { sent: 0, errors: 0, startedAt: Date.now() }
|
|
68
86
|
}
|
|
69
87
|
|
|
@@ -97,6 +115,20 @@ export class Gateway {
|
|
|
97
115
|
return out
|
|
98
116
|
}
|
|
99
117
|
|
|
118
|
+
async sendAlert(type, payload = {}) {
|
|
119
|
+
try {
|
|
120
|
+
const target = resolveAlertTarget(this)
|
|
121
|
+
if (!target) return
|
|
122
|
+
const allowedEvents = this.config.telegram?.alerts?.events || ['error', 'pairing']
|
|
123
|
+
if (type !== 'status' && !allowedEvents.includes(type)) return
|
|
124
|
+
|
|
125
|
+
const text = formatAlertMessage(type, payload)
|
|
126
|
+
await this.sendToMessenger(target, { text })
|
|
127
|
+
} catch (err) {
|
|
128
|
+
this.ctx.logger?.warn?.(`sendAlert (${type}): ${err.message}`)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
100
132
|
async start() {
|
|
101
133
|
this.disposeListener = this.ctx.on('session/event', (session, event) => {
|
|
102
134
|
const collector = this.pending.get(session.id)
|
|
@@ -147,9 +179,11 @@ export class Gateway {
|
|
|
147
179
|
this.idleTimer = setInterval(() => this.reapIdle(), Math.min(idleMs, 60_000))
|
|
148
180
|
this.idleTimer.unref?.()
|
|
149
181
|
}
|
|
182
|
+
this.scheduler.start()
|
|
150
183
|
}
|
|
151
184
|
|
|
152
185
|
stop() {
|
|
186
|
+
if (this.scheduler) this.scheduler.stop()
|
|
153
187
|
if (this.disposeListener) this.disposeListener()
|
|
154
188
|
if (this.idleTimer) clearInterval(this.idleTimer)
|
|
155
189
|
for (const a of this.adapterList) { try { a.stop() } catch {} }
|
|
@@ -195,7 +229,24 @@ export class Gateway {
|
|
|
195
229
|
const adapter = this.getAdapter(target.platform)
|
|
196
230
|
if (!adapter) throw new Error(`adapter ${target.platform} unavailable`)
|
|
197
231
|
const token = makeAskToken()
|
|
198
|
-
const
|
|
232
|
+
const isMulti = payload.mode === 'multi' || (Array.isArray(payload.options) && payload.options.length > 0)
|
|
233
|
+
let replyMarkup
|
|
234
|
+
let callbackKeys
|
|
235
|
+
let selectedSet
|
|
236
|
+
let page = 0
|
|
237
|
+
const pageSize = Number(payload.pageSize) || 6
|
|
238
|
+
|
|
239
|
+
if (isMulti) {
|
|
240
|
+
selectedSet = new Set(Array.isArray(payload.selected) ? payload.selected : [])
|
|
241
|
+
const kb = buildMultiSelectKeyboard(token, payload.options || payload.buttons, selectedSet, page, pageSize, payload)
|
|
242
|
+
replyMarkup = kb.replyMarkup
|
|
243
|
+
callbackKeys = kb.callbackKeys
|
|
244
|
+
} else {
|
|
245
|
+
const kb = buildInlineKeyboard(token, payload.buttons || [])
|
|
246
|
+
replyMarkup = kb.replyMarkup
|
|
247
|
+
callbackKeys = kb.callbackKeys
|
|
248
|
+
}
|
|
249
|
+
|
|
199
250
|
indexCallbacks(this.callbackIndex, callbackKeys, token)
|
|
200
251
|
try {
|
|
201
252
|
await adapter.sendTo(target.chatId, { text: payload.text, replyMarkup }, { threadId: target.threadId })
|
|
@@ -215,7 +266,12 @@ export class Gateway {
|
|
|
215
266
|
reject(new Error('messenger.ask timed out'))
|
|
216
267
|
}, timeoutMs)
|
|
217
268
|
timer.unref?.()
|
|
218
|
-
this.pendingAsks.set(token, {
|
|
269
|
+
this.pendingAsks.set(token, {
|
|
270
|
+
resolve, reject, timer, target, callbackKeys, isMulti,
|
|
271
|
+
options: payload.options || payload.buttons,
|
|
272
|
+
selected: selectedSet,
|
|
273
|
+
page, pageSize, payload,
|
|
274
|
+
})
|
|
219
275
|
})
|
|
220
276
|
}
|
|
221
277
|
|
|
@@ -232,6 +288,7 @@ export class Gateway {
|
|
|
232
288
|
try {
|
|
233
289
|
const { code } = this.pairing.requestCode(userId, { username })
|
|
234
290
|
await reply(`Нет доступа.\nВаш id: ${userId}\nКод сопряжения: ${code}\n\nВладелец должен отправить боту:\n/pair ${code}`)
|
|
291
|
+
this.sendAlert('pairing', { userId, username, code }).catch(() => {})
|
|
235
292
|
} catch (err) {
|
|
236
293
|
if (err.code === 'RATE_LIMIT') await reply('Код уже выдан. Подождите или попросите владельца /pair.')
|
|
237
294
|
else await reply(`Не удалось выдать код: ${err.message}`)
|
|
@@ -248,14 +305,156 @@ export class Gateway {
|
|
|
248
305
|
await cb.answer('Кнопка для другого чата')
|
|
249
306
|
return
|
|
250
307
|
}
|
|
308
|
+
const action = parseAskCallback(cb.data)
|
|
309
|
+
if (pending.isMulti) {
|
|
310
|
+
if (action.kind === 'toggle') {
|
|
311
|
+
if (pending.selected.has(action.id)) pending.selected.delete(action.id)
|
|
312
|
+
else pending.selected.add(action.id)
|
|
313
|
+
releaseCallbacks(this.callbackIndex, pending.callbackKeys)
|
|
314
|
+
const nextKb = buildMultiSelectKeyboard(askToken, pending.options, pending.selected, pending.page, pending.pageSize, pending.payload)
|
|
315
|
+
pending.callbackKeys = nextKb.callbackKeys
|
|
316
|
+
indexCallbacks(this.callbackIndex, nextKb.callbackKeys, askToken)
|
|
317
|
+
try {
|
|
318
|
+
if (cb.editReplyMarkup) await cb.editReplyMarkup(nextKb.replyMarkup)
|
|
319
|
+
else await cb.editMessage(cb.message?.text || 'Выбор', nextKb.replyMarkup)
|
|
320
|
+
} catch {}
|
|
321
|
+
await cb.answer(pending.selected.has(action.id) ? 'Выбрано' : 'Снято')
|
|
322
|
+
return
|
|
323
|
+
}
|
|
324
|
+
if (action.kind === 'page') {
|
|
325
|
+
pending.page = action.page
|
|
326
|
+
releaseCallbacks(this.callbackIndex, pending.callbackKeys)
|
|
327
|
+
const nextKb = buildMultiSelectKeyboard(askToken, pending.options, pending.selected, pending.page, pending.pageSize, pending.payload)
|
|
328
|
+
pending.callbackKeys = nextKb.callbackKeys
|
|
329
|
+
indexCallbacks(this.callbackIndex, nextKb.callbackKeys, askToken)
|
|
330
|
+
try {
|
|
331
|
+
if (cb.editReplyMarkup) await cb.editReplyMarkup(nextKb.replyMarkup)
|
|
332
|
+
else await cb.editMessage(cb.message?.text || 'Выбор', nextKb.replyMarkup)
|
|
333
|
+
} catch {}
|
|
334
|
+
await cb.answer()
|
|
335
|
+
return
|
|
336
|
+
}
|
|
337
|
+
if (action.kind === 'done') {
|
|
338
|
+
clearTimeout(pending.timer)
|
|
339
|
+
this.pendingAsks.delete(askToken)
|
|
340
|
+
releaseCallbacks(this.callbackIndex, pending.callbackKeys)
|
|
341
|
+
await cb.answer('OK')
|
|
342
|
+
try { await cb.editMessage(cb.message?.text || 'Выбрано', REMOVE_KEYBOARD) } catch {}
|
|
343
|
+
pending.resolve({ buttonId: 'done', selected: Array.from(pending.selected), data: cb.data })
|
|
344
|
+
return
|
|
345
|
+
}
|
|
346
|
+
if (action.kind === 'cancel') {
|
|
347
|
+
clearTimeout(pending.timer)
|
|
348
|
+
this.pendingAsks.delete(askToken)
|
|
349
|
+
releaseCallbacks(this.callbackIndex, pending.callbackKeys)
|
|
350
|
+
await cb.answer('Отменено')
|
|
351
|
+
try { await cb.editMessage(cb.message?.text || 'Отменено', REMOVE_KEYBOARD) } catch {}
|
|
352
|
+
pending.resolve({ buttonId: 'cancel', selected: [], data: cb.data })
|
|
353
|
+
return
|
|
354
|
+
}
|
|
355
|
+
}
|
|
251
356
|
clearTimeout(pending.timer)
|
|
252
357
|
this.pendingAsks.delete(askToken)
|
|
253
358
|
releaseCallbacks(this.callbackIndex, pending.callbackKeys)
|
|
254
359
|
await cb.answer('OK')
|
|
255
360
|
try { await cb.editMessage(cb.message?.text || 'Выбрано', REMOVE_KEYBOARD) } catch {}
|
|
256
|
-
pending.resolve({ buttonId, data: cb.data })
|
|
361
|
+
pending.resolve({ buttonId: action.id || buttonId, data: cb.data })
|
|
257
362
|
return
|
|
258
363
|
}
|
|
364
|
+
|
|
365
|
+
// Model picker interactive flow
|
|
366
|
+
if (cb.data?.startsWith('mdl:')) {
|
|
367
|
+
const parts = cb.data.split(':')
|
|
368
|
+
const sub = parts[1]
|
|
369
|
+
// Step 2: Selected provider -> show its models (10 per page)
|
|
370
|
+
if (sub === 'p') {
|
|
371
|
+
const providerId = parts[2]
|
|
372
|
+
const current = this.resolveAgentModel()
|
|
373
|
+
const catalog = await listModelCatalog(this.ctx, current)
|
|
374
|
+
const models = catalog.modelsByProvider.get(providerId) || []
|
|
375
|
+
if (!models.length) {
|
|
376
|
+
await cb.answer('Нет доступных моделей у этого провайдера')
|
|
377
|
+
return
|
|
378
|
+
}
|
|
379
|
+
const kb = buildModelsKeyboard(providerId, models, current.model, 0)
|
|
380
|
+
await cb.answer()
|
|
381
|
+
const text = [
|
|
382
|
+
`🤖 <b>Провайдер:</b> <code>${providerId}</code>`,
|
|
383
|
+
`Выберите модель (страница ${kb.page + 1}/${kb.totalPages}):`,
|
|
384
|
+
].join('\n')
|
|
385
|
+
try {
|
|
386
|
+
if (cb.editMessage) await cb.editMessage(text, kb)
|
|
387
|
+
} catch {}
|
|
388
|
+
return
|
|
389
|
+
}
|
|
390
|
+
// Pagination for models
|
|
391
|
+
if (sub === 'pg') {
|
|
392
|
+
const providerId = parts[2]
|
|
393
|
+
const page = parseInt(parts[3], 10) || 0
|
|
394
|
+
const current = this.resolveAgentModel()
|
|
395
|
+
const catalog = await listModelCatalog(this.ctx, current)
|
|
396
|
+
const models = catalog.modelsByProvider.get(providerId) || []
|
|
397
|
+
const kb = buildModelsKeyboard(providerId, models, current.model, page)
|
|
398
|
+
await cb.answer()
|
|
399
|
+
const text = [
|
|
400
|
+
`🤖 <b>Провайдер:</b> <code>${providerId}</code>`,
|
|
401
|
+
`Выберите модель (страница ${kb.page + 1}/${kb.totalPages}):`,
|
|
402
|
+
].join('\n')
|
|
403
|
+
try {
|
|
404
|
+
if (cb.editMessage) await cb.editMessage(text, kb)
|
|
405
|
+
} catch {}
|
|
406
|
+
return
|
|
407
|
+
}
|
|
408
|
+
// Back to providers
|
|
409
|
+
if (sub === 'back') {
|
|
410
|
+
const current = this.resolveAgentModel()
|
|
411
|
+
const catalog = await listModelCatalog(this.ctx, current)
|
|
412
|
+
const kb = buildProvidersKeyboard(catalog.providers, current)
|
|
413
|
+
await cb.answer()
|
|
414
|
+
const text = [
|
|
415
|
+
'🤖 <b>Выберите провайдера:</b>',
|
|
416
|
+
`Текущая: <code>${current.provider}/${current.model}</code>`,
|
|
417
|
+
].join('\n')
|
|
418
|
+
try {
|
|
419
|
+
if (cb.editMessage) await cb.editMessage(text, kb)
|
|
420
|
+
} catch {}
|
|
421
|
+
return
|
|
422
|
+
}
|
|
423
|
+
// Select model
|
|
424
|
+
if (sub === 's') {
|
|
425
|
+
const key = parts[2]
|
|
426
|
+
const stored = getStoredModelSelection(key)
|
|
427
|
+
if (!stored) {
|
|
428
|
+
await cb.answer('Сессия выбора модели истекла, вызовите /model заново')
|
|
429
|
+
return
|
|
430
|
+
}
|
|
431
|
+
const { provider, model } = stored
|
|
432
|
+
try {
|
|
433
|
+
const adm = this.ctx.get('agentDefaultModel')
|
|
434
|
+
if (adm?.saveSelection) {
|
|
435
|
+
await adm.saveSelection({ provider, model })
|
|
436
|
+
}
|
|
437
|
+
this.config.agent = { ...this.config.agent, provider, model }
|
|
438
|
+
try {
|
|
439
|
+
await this.hooks?.persistAgentModel?.({ provider, model })
|
|
440
|
+
} catch (e) {
|
|
441
|
+
this.ctx.logger?.warn?.(`persist agent model: ${e.message}`)
|
|
442
|
+
}
|
|
443
|
+
await cb.answer(`Выбрана ${model}`)
|
|
444
|
+
try {
|
|
445
|
+
if (cb.editMessage) await cb.editMessage(`✅ Модель успешно переключена на: <b>${provider}/${model}</b>`, REMOVE_KEYBOARD)
|
|
446
|
+
} catch {}
|
|
447
|
+
} catch (err) {
|
|
448
|
+
await cb.answer(`Ошибка: ${err.message}`)
|
|
449
|
+
}
|
|
450
|
+
return
|
|
451
|
+
}
|
|
452
|
+
if (sub === 'cur') {
|
|
453
|
+
await cb.answer()
|
|
454
|
+
return
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
259
458
|
await cb.answer()
|
|
260
459
|
}
|
|
261
460
|
|
|
@@ -300,7 +499,16 @@ export class Gateway {
|
|
|
300
499
|
chat.pendingMedia = []
|
|
301
500
|
}
|
|
302
501
|
const inboundWasVoice = attachments.some((a) => a.kind === 'voice' || a.kind === 'audio')
|
|
303
|
-
|
|
502
|
+
let personaOverride
|
|
503
|
+
for (const [pId] of Object.entries(BUILTIN_PERSONAS)) {
|
|
504
|
+
if (pId === 'default') continue
|
|
505
|
+
const tag = `@${pId}`
|
|
506
|
+
if (body.toLowerCase().includes(tag)) {
|
|
507
|
+
personaOverride = pId
|
|
508
|
+
break
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
const turnInput = { ...input, text: body, attachments, inboundWasVoice, personaOverride }
|
|
304
512
|
|
|
305
513
|
// Hermes-like steer: while a turn is running, inject followup instead of abort+restart
|
|
306
514
|
if (this.isChatBusy(chat)) {
|
|
@@ -328,6 +536,13 @@ export class Gateway {
|
|
|
328
536
|
run.catch((err) => {
|
|
329
537
|
const msg = err instanceof Error ? err.message : String(err)
|
|
330
538
|
this.ctx.logger?.warn?.(`dsh-messenger-gateway: background turn: ${msg}`)
|
|
539
|
+
this.sendAlert('error', {
|
|
540
|
+
code: err?.code || 'BACKGROUND_ERROR',
|
|
541
|
+
message: msg,
|
|
542
|
+
sessionId: chat.agent?.session?.id,
|
|
543
|
+
chatId: input.chatId,
|
|
544
|
+
threadId: input.threadId,
|
|
545
|
+
}).catch(() => {})
|
|
331
546
|
chat.turnActive = false
|
|
332
547
|
chat.abort = undefined
|
|
333
548
|
})
|
|
@@ -353,12 +568,157 @@ export class Gateway {
|
|
|
353
568
|
'/stop — прервать текущий ответ',
|
|
354
569
|
'/status — статус шлюза',
|
|
355
570
|
'/model — текущая модель; /model provider model — сменить',
|
|
571
|
+
'/role [name] — персоны и роли агента (/role list)',
|
|
572
|
+
'/skills / /tools — список инструментов и навыков',
|
|
573
|
+
'/fork — ответвление текущей сессии',
|
|
574
|
+
'/export — выгрузка истории диалога в Markdown',
|
|
575
|
+
'/rewind [N] — откат последних N сообщений',
|
|
576
|
+
'/files [dir] — менеджер файлов в рабочей папке агента',
|
|
577
|
+
'/get <path> — скачать файл из рабочей папки в Telegram',
|
|
356
578
|
'/pair CODE — одобрить пользователя',
|
|
357
579
|
'/sethome [name] — этот чат = home (имя опционально)',
|
|
358
580
|
'/home — список home',
|
|
581
|
+
'/setalert — назначить этот чат каналом алертов',
|
|
582
|
+
'/alert [test] — статус канала алертов и тест',
|
|
583
|
+
'/remind <время> <текст> — напоминание (/remind 10m текст, /remind list)',
|
|
359
584
|
'/voice on|off|status — голосовые ответы',
|
|
585
|
+
'/tts on|off|status — озвучка в этом чате',
|
|
586
|
+
'/mute / /unmute — заглушить уведомления в этот чат',
|
|
587
|
+
'/keyboard on|off — быстрая клавиатура',
|
|
588
|
+
].join('\n'))
|
|
589
|
+
}
|
|
590
|
+
if (cmd === '/role' || cmd === '/persona') {
|
|
591
|
+
const targetRole = parts[1]?.toLowerCase()
|
|
592
|
+
if (!targetRole || targetRole === 'list') {
|
|
593
|
+
const currentId = this.personas.get(chatId)
|
|
594
|
+
const lines = [
|
|
595
|
+
'🎭 <b>Доступные роли и персоны:</b>',
|
|
596
|
+
'',
|
|
597
|
+
...listPersonas().map((p) => {
|
|
598
|
+
const isCurrent = p.id === currentId ? ' (активна)' : ''
|
|
599
|
+
return `${p.icon} <b>${p.id}</b> — ${p.name}: ${p.description}${isCurrent}`
|
|
600
|
+
}),
|
|
601
|
+
'',
|
|
602
|
+
'Смена роли: <code>/role coder</code> (или /role reset)',
|
|
603
|
+
]
|
|
604
|
+
return reply(lines.join('\n'))
|
|
605
|
+
}
|
|
606
|
+
if (targetRole === 'reset' || targetRole === 'default') {
|
|
607
|
+
this.personas.set(chatId, 'default')
|
|
608
|
+
return reply('🎭 Роль сброшена на стандартную (Default).')
|
|
609
|
+
}
|
|
610
|
+
const persona = getPersona(targetRole)
|
|
611
|
+
if (!persona) {
|
|
612
|
+
return reply(`Неизвестная роль "${targetRole}". Список: /role list`)
|
|
613
|
+
}
|
|
614
|
+
this.personas.set(chatId, persona.id)
|
|
615
|
+
return reply(`🎭 Роль изменена на: ${persona.icon} <b>${persona.name}</b>\n${persona.description}`)
|
|
616
|
+
}
|
|
617
|
+
if (cmd === '/skills' || cmd === '/tools') {
|
|
618
|
+
const toolsList = []
|
|
619
|
+
if (this.ctx.tools?.tools) {
|
|
620
|
+
for (const [name, t] of this.ctx.tools.tools.entries()) {
|
|
621
|
+
toolsList.push(`• <b>${name}</b>: ${t.description || '(нет описания)'}`)
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
if (!toolsList.length) {
|
|
625
|
+
return reply('🛠️ <b>Инструменты агента:</b>\n(нет зарегистрированных инструментов)')
|
|
626
|
+
}
|
|
627
|
+
return reply([
|
|
628
|
+
'🛠️ <b>Активные инструменты и скиллы:</b>',
|
|
629
|
+
'',
|
|
630
|
+
...toolsList,
|
|
360
631
|
].join('\n'))
|
|
361
632
|
}
|
|
633
|
+
if (cmd === '/export') {
|
|
634
|
+
const chat = this.chats.get(key)
|
|
635
|
+
if (!chat?.agent?.session) {
|
|
636
|
+
return reply('Нет активной сессии для экспорта.')
|
|
637
|
+
}
|
|
638
|
+
try {
|
|
639
|
+
const { filename, buffer, messagesCount } = exportSessionToMarkdown(chat.agent.session)
|
|
640
|
+
if (!messagesCount) {
|
|
641
|
+
return reply('Сессия пуста, нет сообщений для экспорта.')
|
|
642
|
+
}
|
|
643
|
+
const file = {
|
|
644
|
+
name: filename,
|
|
645
|
+
mime: 'text/markdown',
|
|
646
|
+
kind: 'document',
|
|
647
|
+
bytes: buffer,
|
|
648
|
+
dataBase64: buffer.toString('base64'),
|
|
649
|
+
}
|
|
650
|
+
return reply({ text: `📄 Экспорт диалога (${messagesCount} сообщений):`, files: [file] })
|
|
651
|
+
} catch (err) {
|
|
652
|
+
return reply(`Ошибка экспорта: ${err.message}`)
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
if (cmd === '/rewind') {
|
|
656
|
+
const chat = this.chats.get(key)
|
|
657
|
+
if (!chat?.agent?.session) {
|
|
658
|
+
return reply('Нет активной сессии для отката.')
|
|
659
|
+
}
|
|
660
|
+
const count = Number(parts[1]) || 1
|
|
661
|
+
const res = rewindSession(chat.agent.session, count)
|
|
662
|
+
if (!res.removed) {
|
|
663
|
+
return reply('В истории сессии нет сообщений для отката.')
|
|
664
|
+
}
|
|
665
|
+
try { await this.ctx.sessions?.flush(chat.agent.session) } catch {}
|
|
666
|
+
return reply(`⏪ Откачено сообщений: ${res.removed}. Осталось в контексте: ${res.remaining}.`)
|
|
667
|
+
}
|
|
668
|
+
if (cmd === '/fork') {
|
|
669
|
+
const chat = this.chats.get(key)
|
|
670
|
+
if (!chat?.agent?.session) {
|
|
671
|
+
return reply('Нет активной сессии для форка.')
|
|
672
|
+
}
|
|
673
|
+
try {
|
|
674
|
+
const oldSession = chat.agent.session
|
|
675
|
+
const oldMessages = Array.isArray(oldSession.messages) ? [...oldSession.messages] : []
|
|
676
|
+
const newChat = await this.createChat(key, input)
|
|
677
|
+
if (newChat.agent?.session && oldMessages.length) {
|
|
678
|
+
newChat.agent.session.messages = oldMessages
|
|
679
|
+
try { await this.ctx.sessions?.flush(newChat.agent.session) } catch {}
|
|
680
|
+
}
|
|
681
|
+
this.chats.set(key, newChat)
|
|
682
|
+
return reply(`🔀 Создан форк сессии!\nСтарая сессия: ${oldSession.id}\nНовая активная сессия: ${newChat.agent.session.id}\nКонтекст сохранён (${oldMessages.length} сообщений).`)
|
|
683
|
+
} catch (err) {
|
|
684
|
+
return reply(`Ошибка форка: ${err.message}`)
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
if (cmd === '/files') {
|
|
688
|
+
const subPath = parts.slice(1).join(' ').trim() || '.'
|
|
689
|
+
const agentCwd = this.config.agent?.cwd || process.cwd()
|
|
690
|
+
const res = await listFiles(agentCwd, subPath)
|
|
691
|
+
if (!res.ok) return reply(`❌ ${res.error}`)
|
|
692
|
+
return reply(res.formattedText)
|
|
693
|
+
}
|
|
694
|
+
if (cmd === '/get') {
|
|
695
|
+
const targetRel = parts.slice(1).join(' ').trim()
|
|
696
|
+
if (!targetRel) {
|
|
697
|
+
return reply('Укажите имя файла для скачивания: <code>/get <файл></code>\nСписок: <code>/files</code>')
|
|
698
|
+
}
|
|
699
|
+
const agentCwd = this.config.agent?.cwd || process.cwd()
|
|
700
|
+
const maxDocBytes = Number(this.config.media?.maxDocBytes) || 50 * 1024 * 1024
|
|
701
|
+
const res = await getFileForDownload(agentCwd, targetRel, maxDocBytes)
|
|
702
|
+
if (!res.ok) return reply(`❌ ${res.error}`)
|
|
703
|
+
const file = {
|
|
704
|
+
name: res.name,
|
|
705
|
+
mime: res.mime,
|
|
706
|
+
kind: 'document',
|
|
707
|
+
bytes: res.bytes,
|
|
708
|
+
dataBase64: res.bytes.toString('base64'),
|
|
709
|
+
}
|
|
710
|
+
return reply({ text: `📄 Файл: <b>${res.name}</b> (${formatFileSize(res.size)})`, files: [file] })
|
|
711
|
+
}
|
|
712
|
+
if (cmd === '/keyboard') {
|
|
713
|
+
const sub = String(parts[1] || 'status').toLowerCase()
|
|
714
|
+
const tgAdapter = this.getAdapter('telegram')
|
|
715
|
+
if (sub === 'on' || sub === 'off') {
|
|
716
|
+
if (tgAdapter) tgAdapter.quickActions = sub === 'on'
|
|
717
|
+
return reply(sub === 'on' ? 'Быстрая клавиатура: включена.' : 'Быстрая клавиатура: выключена.', sub === 'off' ? { replyMarkup: { remove_keyboard: true } } : undefined)
|
|
718
|
+
}
|
|
719
|
+
const cur = tgAdapter?.quickActions !== false
|
|
720
|
+
return reply(`Быстрая клавиатура: ${cur ? 'on' : 'off'}\nСмена: /keyboard on|off`)
|
|
721
|
+
}
|
|
362
722
|
if (cmd === '/new') {
|
|
363
723
|
const chat = this.chats.get(key)
|
|
364
724
|
if (chat) {
|
|
@@ -441,8 +801,18 @@ export class Gateway {
|
|
|
441
801
|
}
|
|
442
802
|
}
|
|
443
803
|
try {
|
|
444
|
-
const
|
|
445
|
-
|
|
804
|
+
const current = this.resolveAgentModel()
|
|
805
|
+
const catalog = await listModelCatalog(this.ctx, current)
|
|
806
|
+
if (!catalog.providers.length) {
|
|
807
|
+
return reply(`Текущая модель: <code>${current.provider}/${current.model}</code>\nСмена: <code>/model <provider> <model></code>`)
|
|
808
|
+
}
|
|
809
|
+
const kb = buildProvidersKeyboard(catalog.providers, current)
|
|
810
|
+
return reply([
|
|
811
|
+
'🤖 <b>Выберите провайдера:</b>',
|
|
812
|
+
`Текущая модель: <code>${current.provider}/${current.model}</code>`,
|
|
813
|
+
].join('\n'), {
|
|
814
|
+
replyMarkup: kb,
|
|
815
|
+
})
|
|
446
816
|
} catch (e) {
|
|
447
817
|
return reply(e.message)
|
|
448
818
|
}
|
|
@@ -477,8 +847,101 @@ export class Gateway {
|
|
|
477
847
|
if (!homes.length) return reply('Home не задан. /sethome или /sethome name')
|
|
478
848
|
return reply(['Homes:', ...homes.map((h) => `• ${h.name}: chat ${h.chatId}${h.threadId ? ` topic ${h.threadId}` : ''}`)].join('\n'))
|
|
479
849
|
}
|
|
850
|
+
if (cmd === '/setalert') {
|
|
851
|
+
if (!this.isUserAllowed(userId)) return reply('Нет доступа.')
|
|
852
|
+
const nextTg = {
|
|
853
|
+
...this.tg(),
|
|
854
|
+
alerts: {
|
|
855
|
+
...(this.tg().alerts || {}),
|
|
856
|
+
enabled: true,
|
|
857
|
+
chatId,
|
|
858
|
+
threadId: threadId || 0,
|
|
859
|
+
},
|
|
860
|
+
}
|
|
861
|
+
this.config.telegram = nextTg
|
|
862
|
+
try { await this.hooks?.persistHomes?.(nextTg) } catch {}
|
|
863
|
+
return reply(`🔔 Этот чат назначен каналом алертов (chat: ${chatId}${threadId ? `, topic: ${threadId}` : ''}).`)
|
|
864
|
+
}
|
|
865
|
+
if (cmd === '/alert') {
|
|
866
|
+
const sub = parts[1]?.toLowerCase()
|
|
867
|
+
if (sub === 'test') {
|
|
868
|
+
const target = resolveAlertTarget(this)
|
|
869
|
+
if (!target) return reply('Канал алертов не настроен или выключен. Назначить: /setalert')
|
|
870
|
+
await this.sendAlert('status', { title: 'Тестовый алерт', details: `Отправлен пользователем id ${userId}` })
|
|
871
|
+
return reply('Тестовый алерт отправлен в канал алертов.')
|
|
872
|
+
}
|
|
873
|
+
const target = resolveAlertTarget(this)
|
|
874
|
+
const alertsCfg = this.tg().alerts || {}
|
|
875
|
+
return reply([
|
|
876
|
+
'🔔 <b>Канал алертов:</b>',
|
|
877
|
+
`Статус: ${alertsCfg.enabled ? 'включен' : 'выключен'}`,
|
|
878
|
+
`Чат: ${target ? `${target.chatId}${target.threadId ? ` (topic: ${target.threadId})` : ''}` : '(не назначен)'}`,
|
|
879
|
+
`События: ${(alertsCfg.events || ['error', 'pairing']).join(', ')}`,
|
|
880
|
+
'',
|
|
881
|
+
'Команды:',
|
|
882
|
+
'/setalert — назначить текущий чат каналом алертов',
|
|
883
|
+
'/alert test — отправить тестовый алерт',
|
|
884
|
+
].join('\n'))
|
|
885
|
+
}
|
|
886
|
+
if (cmd === '/remind') {
|
|
887
|
+
const sub = parts[1]?.toLowerCase()
|
|
888
|
+
if (sub === 'list') {
|
|
889
|
+
const active = await this.scheduler.list(chatId)
|
|
890
|
+
if (!active.length) return reply('Нет активных напоминаний для этого чата.')
|
|
891
|
+
const lines = [
|
|
892
|
+
'⏰ <b>Активные напоминания:</b>',
|
|
893
|
+
'',
|
|
894
|
+
...active.map((t) => {
|
|
895
|
+
const left = formatRemaining(t.dueAt - Date.now())
|
|
896
|
+
return `• <code>${t.id}</code> (через ${left}): ${t.text}`
|
|
897
|
+
}),
|
|
898
|
+
'',
|
|
899
|
+
'Отмена: <code>/remind cancel ID</code>',
|
|
900
|
+
]
|
|
901
|
+
return reply(lines.join('\n'))
|
|
902
|
+
}
|
|
903
|
+
if (sub === 'cancel') {
|
|
904
|
+
const targetId = parts[2]?.trim()
|
|
905
|
+
if (!targetId) return reply('Укажите ID напоминания: <code>/remind cancel ID</code>')
|
|
906
|
+
const ok = await this.scheduler.cancel(targetId, chatId)
|
|
907
|
+
return reply(ok ? `✅ Напоминание <code>${targetId}</code> отменено.` : `❌ Напоминание с ID <code>${targetId}</code> не найдено.`)
|
|
908
|
+
}
|
|
909
|
+
const timeArg = parts[1]
|
|
910
|
+
const textArg = parts.slice(2).join(' ').trim()
|
|
911
|
+
const delayMs = parseRelativeTime(timeArg)
|
|
912
|
+
if (!delayMs || !textArg) {
|
|
913
|
+
return reply([
|
|
914
|
+
'⏰ <b>Напоминания:</b>',
|
|
915
|
+
'Создать: <code>/remind <время> <текст></code>',
|
|
916
|
+
'Примеры: <code>/remind 10m Позвонить</code>, <code>/remind 2h Проверить деплой</code>',
|
|
917
|
+
'Список: <code>/remind list</code>',
|
|
918
|
+
'Отмена: <code>/remind cancel ID</code>',
|
|
919
|
+
].join('\n'))
|
|
920
|
+
}
|
|
921
|
+
const dueAt = Date.now() + delayMs
|
|
922
|
+
const task = await this.scheduler.schedule({
|
|
923
|
+
platform: platform || 'telegram',
|
|
924
|
+
chatId,
|
|
925
|
+
threadId: threadId || 0,
|
|
926
|
+
userId,
|
|
927
|
+
text: textArg,
|
|
928
|
+
dueAt,
|
|
929
|
+
})
|
|
930
|
+
const left = formatRemaining(delayMs)
|
|
931
|
+
return reply(`⏰ Напоминание установлено на <b>через ${left}</b> (ID: <code>${task.id}</code>):\n«${textArg}»`)
|
|
932
|
+
}
|
|
480
933
|
if (cmd === '/voice') {
|
|
481
934
|
const sub = String(parts[1] || 'status').toLowerCase()
|
|
935
|
+
if (sub === 'summary') {
|
|
936
|
+
const val = parts[2]?.toLowerCase()
|
|
937
|
+
if (val === 'on' || val === 'off') {
|
|
938
|
+
if (!this.config.tts) this.config.tts = {}
|
|
939
|
+
this.config.tts.voiceSummary = val === 'on'
|
|
940
|
+
return reply(`Голосовое саммари (TL;DR): ${val === 'on' ? 'включено' : 'выключено'}`)
|
|
941
|
+
}
|
|
942
|
+
const state = this.config.tts?.voiceSummary ? 'on' : 'off'
|
|
943
|
+
return reply(`Голосовое саммари (TL;DR): ${state}\nПереключение: <code>/voice summary on|off</code>`)
|
|
944
|
+
}
|
|
482
945
|
if (sub === 'on' || sub === 'off') {
|
|
483
946
|
this.voicePrefs.set(userId, sub === 'on')
|
|
484
947
|
return reply(sub === 'on' ? 'Голосовые ответы: on (для вас)' : 'Голосовые ответы: off (для вас)')
|
|
@@ -486,7 +949,84 @@ export class Gateway {
|
|
|
486
949
|
const pref = this.voicePrefs.get(userId)
|
|
487
950
|
const mode = this.tg().voiceMode || 'mirror'
|
|
488
951
|
const prefLine = pref === null ? 'не задан (/voice on|off)' : (pref ? 'on' : 'off')
|
|
489
|
-
|
|
952
|
+
const summaryState = this.config.tts?.voiceSummary ? 'on' : 'off'
|
|
953
|
+
return reply(`voiceMode=${mode}\nваш /voice: ${prefLine}\nglobal tts: ${this.config.tts?.enabled ? 'on' : 'off'}\nvoice summary: ${summaryState}`)
|
|
954
|
+
}
|
|
955
|
+
if (cmd === '/topic') {
|
|
956
|
+
const topicName = parts.slice(1).join(' ').trim()
|
|
957
|
+
if (!topicName) {
|
|
958
|
+
return reply('Использование: <code>/topic <название></code>\nСоздает новый топик в супергруппе и изолированную сессию под задачу.')
|
|
959
|
+
}
|
|
960
|
+
const tgAdapter = this.getAdapter('telegram')
|
|
961
|
+
if (!tgAdapter?.createForumTopic) {
|
|
962
|
+
return reply('Создание топиков доступно только в Telegram.')
|
|
963
|
+
}
|
|
964
|
+
try {
|
|
965
|
+
const res = await tgAdapter.createForumTopic(chatId, topicName)
|
|
966
|
+
const newThreadId = res?.message_thread_id
|
|
967
|
+
await reply(`🎯 Создан новый топик <b>«${topicName}»</b> (ID: <code>${newThreadId}</code>).\nПерейдите в созданный топик для работы над задачей!`)
|
|
968
|
+
if (newThreadId) {
|
|
969
|
+
await tgAdapter.sendTo(chatId, {
|
|
970
|
+
text: `👋 Привет! Это изолированная сессия для задачи <b>«${topicName}»</b>.\nЧем могу помочь?`,
|
|
971
|
+
}, { threadId: newThreadId })
|
|
972
|
+
}
|
|
973
|
+
return
|
|
974
|
+
} catch (err) {
|
|
975
|
+
return reply(`Не удалось создать топик: ${err.message}\n(Убедитесь, что бот является администратором группы и включены темы/форумы)`)
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
if (cmd === '/top') {
|
|
979
|
+
const mem = process.memoryUsage()
|
|
980
|
+
const rssMb = (mem.rss / 1024 / 1024).toFixed(1)
|
|
981
|
+
const heapMb = (mem.heapUsed / 1024 / 1024).toFixed(1)
|
|
982
|
+
const sec = Math.floor((Date.now() - this.stats.startedAt) / 1000)
|
|
983
|
+
const hh = String(Math.floor(sec / 3600)).padStart(2, '0')
|
|
984
|
+
const mm = String(Math.floor((sec % 3600) / 60)).padStart(2, '0')
|
|
985
|
+
const ss = String(sec % 60).padStart(2, '0')
|
|
986
|
+
let activeReminders = 0
|
|
987
|
+
try { activeReminders = (await this.scheduler.list()).length } catch {}
|
|
988
|
+
let currentModel = 'не задана'
|
|
989
|
+
try {
|
|
990
|
+
const m = this.resolveAgentModel()
|
|
991
|
+
currentModel = `${m.provider}/${m.model}`
|
|
992
|
+
} catch {}
|
|
993
|
+
|
|
994
|
+
return reply([
|
|
995
|
+
'📊 <b>DSH System & Resources:</b>',
|
|
996
|
+
`• <b>Память (RSS):</b> ${rssMb} MB`,
|
|
997
|
+
`• <b>Heap:</b> ${heapMb} MB`,
|
|
998
|
+
`• <b>Аптайм:</b> ${hh}:${mm}:${ss}`,
|
|
999
|
+
`• <b>Активных чатов:</b> ${this.chats.size}`,
|
|
1000
|
+
`• <b>Напоминаний в очереди:</b> ${activeReminders}`,
|
|
1001
|
+
`• <b>Активная модель:</b> <code>${currentModel}</code>`,
|
|
1002
|
+
`• <b>Сообщений отправлено:</b> ${this.stats.sent}`,
|
|
1003
|
+
`• <b>Ошибок:</b> ${this.stats.errors}`,
|
|
1004
|
+
].join('\n'))
|
|
1005
|
+
}
|
|
1006
|
+
if (cmd === '/keyboard') {
|
|
1007
|
+
const sub = String(parts[1] || '').toLowerCase()
|
|
1008
|
+
const tgAdapter = this.getAdapter('telegram')
|
|
1009
|
+
if (sub === 'on') {
|
|
1010
|
+
if (tgAdapter) tgAdapter.quickActions = true
|
|
1011
|
+
return reply('Клавиатура быстрых действий включена.', {
|
|
1012
|
+
replyMarkup: buildQuickActionsKeyboard(),
|
|
1013
|
+
})
|
|
1014
|
+
}
|
|
1015
|
+
if (sub === 'off') {
|
|
1016
|
+
if (tgAdapter) tgAdapter.quickActions = false
|
|
1017
|
+
return reply('Клавиатура быстрых действий выключена.', {
|
|
1018
|
+
replyMarkup: REMOVE_REPLY_KEYBOARD,
|
|
1019
|
+
})
|
|
1020
|
+
}
|
|
1021
|
+
const curState = tgAdapter?.quickActions ? 'включена' : 'выключена'
|
|
1022
|
+
return reply([
|
|
1023
|
+
'⌨️ <b>Клавиатура быстрых действий:</b>',
|
|
1024
|
+
`Текущее состояние: <b>${curState}</b>`,
|
|
1025
|
+
'',
|
|
1026
|
+
'Команды:',
|
|
1027
|
+
'<code>/keyboard on</code> — показать кнопки',
|
|
1028
|
+
'<code>/keyboard off</code> — скрыть кнопки',
|
|
1029
|
+
].join('\n'))
|
|
490
1030
|
}
|
|
491
1031
|
if (cmd === '/tts') {
|
|
492
1032
|
const sub = String(parts[1] || 'status').toLowerCase()
|
|
@@ -574,9 +1114,14 @@ export class Gateway {
|
|
|
574
1114
|
}
|
|
575
1115
|
|
|
576
1116
|
async buildUserContent(input, signal) {
|
|
577
|
-
const { text, attachments = [], replyText, steer } = input
|
|
1117
|
+
const { text, attachments = [], replyText, steer, personaOverride } = input
|
|
578
1118
|
const parts = []
|
|
579
1119
|
parts.push(String(this.config.agent?.instructionPrefix || MESSENGER_RELAY_INSTRUCTION))
|
|
1120
|
+
const activePersonaId = personaOverride || this.personas.get(input.chatId)
|
|
1121
|
+
const activePersona = getPersona(activePersonaId)
|
|
1122
|
+
if (activePersona?.instruction) {
|
|
1123
|
+
parts.push(`[Persona: ${activePersona.name} (${activePersona.icon})]\n${activePersona.instruction}`)
|
|
1124
|
+
}
|
|
580
1125
|
if (steer) parts.push('[Steer / дополнение к текущему ходу — учти вместе с предыдущим запросом, не начинай ответ заново с нуля]')
|
|
581
1126
|
if (replyText?.trim()) parts.push(`[Ответ на сообщение: ${replyText.trim()}]`)
|
|
582
1127
|
const blocks = []
|
|
@@ -606,7 +1151,14 @@ export class Gateway {
|
|
|
606
1151
|
parts.push(`[Голосовое сообщение (dsh-voice недоступен: ${msg})]`)
|
|
607
1152
|
}
|
|
608
1153
|
} else if (att.kind === 'document' || att.kind === 'video' || att.kind === 'animation' || att.kind === 'sticker') {
|
|
609
|
-
|
|
1154
|
+
let parsed = null
|
|
1155
|
+
if (att.kind === 'document' && att.path) {
|
|
1156
|
+
try {
|
|
1157
|
+
const maxDocBytes = Number(this.config.media?.maxTextInjectBytes) || 100 * 1024
|
|
1158
|
+
parsed = await parseDocument(att.path, { maxBytes: maxDocBytes })
|
|
1159
|
+
} catch {}
|
|
1160
|
+
}
|
|
1161
|
+
parts.push(formatInboundDocument(att, parsed))
|
|
610
1162
|
} else {
|
|
611
1163
|
parts.push(`[Файл: ${att.path}${att.name ? ` (${att.name})` : ''}]`)
|
|
612
1164
|
}
|
|
@@ -688,16 +1240,28 @@ export class Gateway {
|
|
|
688
1240
|
if (progress) try { await progress.remove() } catch {}
|
|
689
1241
|
progress = null
|
|
690
1242
|
if (typeof react === 'function') react('').catch?.(() => {})
|
|
691
|
-
const
|
|
1243
|
+
const rawAnswer = stripReasoningPreamble(stripImageUrls(collector.lastText || collector.streamText || collector.parts.join('\n\n')))
|
|
1244
|
+
const processed = processDiagramsAndTables(rawAnswer, {
|
|
1245
|
+
artifactPreviews: this.tg().artifactPreviews !== false,
|
|
1246
|
+
})
|
|
1247
|
+
const answer = processed.text
|
|
692
1248
|
if (collector.reason?.kind === 'error') {
|
|
693
1249
|
const err = collector.reason.error
|
|
694
1250
|
const msg = `Ошибка агента: ${err?.code || 'error'}: ${err?.message || 'unknown'}`
|
|
1251
|
+
this.sendAlert('error', {
|
|
1252
|
+
code: err?.code || 'AGENT_ERROR',
|
|
1253
|
+
message: err?.message || 'unknown',
|
|
1254
|
+
sessionId,
|
|
1255
|
+
chatId: input.chatId,
|
|
1256
|
+
threadId: input.threadId,
|
|
1257
|
+
}).catch(() => {})
|
|
695
1258
|
if (stream) { await scheduler?.flush(); await stream.finalize(msg) }
|
|
696
1259
|
else await reply(msg)
|
|
697
1260
|
return
|
|
698
1261
|
}
|
|
699
1262
|
const files = await buildOutboundFiles(this.ctx, this.baseUrl(), collector, { signal, logger: this.ctx.logger })
|
|
700
|
-
|
|
1263
|
+
const allFiles = [...files, ...(processed.files || [])]
|
|
1264
|
+
if (!answer && !allFiles.length) {
|
|
701
1265
|
if (stream) { await scheduler?.flush(); await stream.finalize('(нет ответа)') }
|
|
702
1266
|
else await reply('(нет ответа)')
|
|
703
1267
|
return
|
|
@@ -708,10 +1272,10 @@ export class Gateway {
|
|
|
708
1272
|
await scheduler?.flush()
|
|
709
1273
|
await stream.finalize(chunks[0] || '(нет ответа)')
|
|
710
1274
|
for (let i = 1; i < chunks.length; i++) await reply({ text: chunks[i] })
|
|
711
|
-
if (
|
|
1275
|
+
if (allFiles.length) await reply({ files: allFiles })
|
|
712
1276
|
} else {
|
|
713
1277
|
for (let i = 0; i < chunks.length; i++) {
|
|
714
|
-
await reply({ text: chunks[i] || undefined, files: i === 0 ?
|
|
1278
|
+
await reply({ text: chunks[i] || undefined, files: i === 0 ? allFiles : [] })
|
|
715
1279
|
}
|
|
716
1280
|
}
|
|
717
1281
|
const chatTtsPref = this.chatTts.get(chat.target?.chatId)
|
|
@@ -723,7 +1287,8 @@ export class Gateway {
|
|
|
723
1287
|
chatPref: chatTtsPref,
|
|
724
1288
|
})
|
|
725
1289
|
if (speak && !signal.aborted) {
|
|
726
|
-
const
|
|
1290
|
+
const isVoiceSummary = this.config.tts?.voiceSummary === true
|
|
1291
|
+
const ttsText = prepareTtsText(answer, this.config.tts?.maxChars, { voiceSummary: isVoiceSummary })
|
|
727
1292
|
if (ttsText) {
|
|
728
1293
|
try {
|
|
729
1294
|
const spoken = await speakText(this.baseUrl(), ttsText, signal)
|
|
@@ -736,6 +1301,13 @@ export class Gateway {
|
|
|
736
1301
|
}
|
|
737
1302
|
} catch (err) {
|
|
738
1303
|
if (!signal?.aborted) {
|
|
1304
|
+
this.sendAlert('error', {
|
|
1305
|
+
code: err?.code || 'EXCEPTION',
|
|
1306
|
+
message: err?.message || String(err),
|
|
1307
|
+
sessionId,
|
|
1308
|
+
chatId: input.chatId,
|
|
1309
|
+
threadId: input.threadId,
|
|
1310
|
+
}).catch(() => {})
|
|
739
1311
|
try {
|
|
740
1312
|
if (stream) await stream.finalize(`Сбой: ${err.message}`)
|
|
741
1313
|
else await reply(`Сбой: ${err.message}`)
|