@goodandready/dsh-messenger-gateway 0.3.14 → 0.3.18
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 +6 -0
- package/lib/adapters/telegram.js +46 -3
- package/lib/client.js +1307 -629
- package/lib/content-guard.js +14 -0
- package/lib/gateway.js +44 -14
- package/lib/index.js +49 -15
- package/lib/models.js +4 -3
- package/lib/outbound.js +2 -1
- package/lib/photos.js +2 -1
- package/lib/session-ops.js +14 -4
- package/lib/telegram-errors.js +51 -41
- package/package.json +2 -5
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize any content value to ContentBlock[].
|
|
3
|
+
* DSH core (dsh-llm) calls content.some() without Array.isArray guard,
|
|
4
|
+
* so we must ensure every message we create or copy has array content.
|
|
5
|
+
* Refs: #51
|
|
6
|
+
*/
|
|
7
|
+
export function ensureContentArray(content) {
|
|
8
|
+
if (Array.isArray(content)) return content
|
|
9
|
+
if (typeof content === 'string') {
|
|
10
|
+
return content ? [{ type: 'text', text: content }] : [{ type: 'text', text: '(пустое сообщение)' }]
|
|
11
|
+
}
|
|
12
|
+
if (content && typeof content === 'object' && content.type) return [content]
|
|
13
|
+
return [{ type: 'text', text: String(content ?? '(пустое сообщение)') }]
|
|
14
|
+
}
|
package/lib/gateway.js
CHANGED
|
@@ -33,6 +33,8 @@ import {
|
|
|
33
33
|
createEditScheduler, startTypingHeartbeat,
|
|
34
34
|
} from './stream.js'
|
|
35
35
|
import { isTopicGoneError } from './telegram-errors.js'
|
|
36
|
+
import { ensureContentArray } from './content-guard.js'
|
|
37
|
+
|
|
36
38
|
|
|
37
39
|
function whenIdleWithTimeout(agent, timeoutMs, signal) {
|
|
38
40
|
const idle = agent.whenIdle()
|
|
@@ -338,8 +340,8 @@ export class Gateway {
|
|
|
338
340
|
return
|
|
339
341
|
}
|
|
340
342
|
if (action.kind === 'done') {
|
|
341
|
-
clearTimeout(pending.timer)
|
|
342
343
|
this.pendingAsks.delete(askToken)
|
|
344
|
+
clearTimeout(pending.timer)
|
|
343
345
|
releaseCallbacks(this.callbackIndex, pending.callbackKeys)
|
|
344
346
|
await cb.answer('OK')
|
|
345
347
|
try { await cb.editMessage(cb.message?.text || 'Выбрано', REMOVE_KEYBOARD) } catch {}
|
|
@@ -347,8 +349,8 @@ export class Gateway {
|
|
|
347
349
|
return
|
|
348
350
|
}
|
|
349
351
|
if (action.kind === 'cancel') {
|
|
350
|
-
clearTimeout(pending.timer)
|
|
351
352
|
this.pendingAsks.delete(askToken)
|
|
353
|
+
clearTimeout(pending.timer)
|
|
352
354
|
releaseCallbacks(this.callbackIndex, pending.callbackKeys)
|
|
353
355
|
await cb.answer('Отменено')
|
|
354
356
|
try { await cb.editMessage(cb.message?.text || 'Отменено', REMOVE_KEYBOARD) } catch {}
|
|
@@ -356,8 +358,8 @@ export class Gateway {
|
|
|
356
358
|
return
|
|
357
359
|
}
|
|
358
360
|
}
|
|
359
|
-
clearTimeout(pending.timer)
|
|
360
361
|
this.pendingAsks.delete(askToken)
|
|
362
|
+
clearTimeout(pending.timer)
|
|
361
363
|
releaseCallbacks(this.callbackIndex, pending.callbackKeys)
|
|
362
364
|
await cb.answer('OK')
|
|
363
365
|
try { await cb.editMessage(cb.message?.text || 'Выбрано', REMOVE_KEYBOARD) } catch {}
|
|
@@ -522,7 +524,7 @@ export class Gateway {
|
|
|
522
524
|
steer: true,
|
|
523
525
|
}, undefined)
|
|
524
526
|
chat.agent.followup(createUserMessage({
|
|
525
|
-
content,
|
|
527
|
+
content: ensureContentArray(content),
|
|
526
528
|
source: { kind: 'plugin', plugin: PLUGIN, form: 'steer' },
|
|
527
529
|
}))
|
|
528
530
|
chat.lastUsed = Date.now()
|
|
@@ -532,6 +534,7 @@ export class Gateway {
|
|
|
532
534
|
|
|
533
535
|
// Mark busy BEFORE yielding to the poll loop, otherwise steer/stop never see an active turn.
|
|
534
536
|
chat.turnActive = true
|
|
537
|
+
try { chat.abort?.abort?.() } catch {}
|
|
535
538
|
chat.abort = new AbortController()
|
|
536
539
|
const run = chat.busy.then(() => this.runTurn(chat, turnInput, chat.abort.signal))
|
|
537
540
|
chat.busy = run.catch(() => {})
|
|
@@ -625,9 +628,9 @@ export class Gateway {
|
|
|
625
628
|
return reply(`🎭 Роль изменена на: ${persona.icon} <b>${persona.name}</b>\n${persona.description}`)
|
|
626
629
|
}
|
|
627
630
|
if (cmd === '/skills' || cmd === '/tools') {
|
|
628
|
-
const
|
|
629
|
-
if (
|
|
630
|
-
for (const [name, t] of
|
|
631
|
+
const tools = this.ctx.get?.('tools') || this.ctx.tools
|
|
632
|
+
if (tools?.tools) {
|
|
633
|
+
for (const [name, t] of tools.tools.entries()) {
|
|
631
634
|
toolsList.push(`• <b>${name}</b>: ${t.description || '(нет описания)'}`)
|
|
632
635
|
}
|
|
633
636
|
}
|
|
@@ -655,7 +658,6 @@ export class Gateway {
|
|
|
655
658
|
mime: 'text/markdown',
|
|
656
659
|
kind: 'document',
|
|
657
660
|
bytes: buffer,
|
|
658
|
-
dataBase64: buffer.toString('base64'),
|
|
659
661
|
}
|
|
660
662
|
return reply({ text: `📄 Экспорт диалога (${messagesCount} сообщений):`, files: [file] })
|
|
661
663
|
} catch (err) {
|
|
@@ -672,7 +674,8 @@ export class Gateway {
|
|
|
672
674
|
if (!res.removed) {
|
|
673
675
|
return reply('В истории сессии нет сообщений для отката.')
|
|
674
676
|
}
|
|
675
|
-
|
|
677
|
+
const sessions = this.ctx.get?.('sessions') || this.ctx.sessions
|
|
678
|
+
try { await sessions?.flush(chat.agent.session) } catch {}
|
|
676
679
|
return reply(`⏪ Откачено сообщений: ${res.removed}. Осталось в контексте: ${res.remaining}.`)
|
|
677
680
|
}
|
|
678
681
|
if (cmd === '/fork') {
|
|
@@ -682,11 +685,14 @@ export class Gateway {
|
|
|
682
685
|
}
|
|
683
686
|
try {
|
|
684
687
|
const oldSession = chat.agent.session
|
|
685
|
-
const oldMessages = Array.isArray(oldSession.messages)
|
|
688
|
+
const oldMessages = Array.isArray(oldSession.messages)
|
|
689
|
+
? oldSession.messages.map(m => ({ ...m, content: ensureContentArray(m.content) }))
|
|
690
|
+
: []
|
|
686
691
|
const newChat = await this.createChat(key, input)
|
|
687
692
|
if (newChat.agent?.session && oldMessages.length) {
|
|
688
693
|
newChat.agent.session.messages = oldMessages
|
|
689
|
-
|
|
694
|
+
const sessions = this.ctx.get?.('sessions') || this.ctx.sessions
|
|
695
|
+
try { await sessions?.flush(newChat.agent.session) } catch {}
|
|
690
696
|
}
|
|
691
697
|
this.chats.set(key, newChat)
|
|
692
698
|
return reply(`🔀 Создан форк сессии!\nСтарая сессия: ${oldSession.id}\nНовая активная сессия: ${newChat.agent.session.id}\nКонтекст сохранён (${oldMessages.length} сообщений).`)
|
|
@@ -1074,7 +1080,8 @@ export class Gateway {
|
|
|
1074
1080
|
const agentCfg = this.config.agent || {}
|
|
1075
1081
|
const cwd = agentCfg.cwd || process.cwd()
|
|
1076
1082
|
const self = this
|
|
1077
|
-
const
|
|
1083
|
+
const agents = this.ctx.get?.('agents') || this.ctx.agents
|
|
1084
|
+
const handle = await agents.create({
|
|
1078
1085
|
sessionId: SessionId(`msgw-${randomUUID()}`),
|
|
1079
1086
|
meta: { cwd },
|
|
1080
1087
|
agentOptions: { provider, model },
|
|
@@ -1224,7 +1231,7 @@ export class Gateway {
|
|
|
1224
1231
|
|
|
1225
1232
|
const content = await this.buildUserContent(input, signal)
|
|
1226
1233
|
chat.agent.followup(createUserMessage({
|
|
1227
|
-
content,
|
|
1234
|
+
content: ensureContentArray(content),
|
|
1228
1235
|
source: { kind: 'plugin', plugin: PLUGIN, form: 'relay' },
|
|
1229
1236
|
}))
|
|
1230
1237
|
const turnTimeoutMs = Number(this.config.agent?.turnTimeoutMs) || 600_000
|
|
@@ -1236,7 +1243,8 @@ export class Gateway {
|
|
|
1236
1243
|
else return reply('Прервано.')
|
|
1237
1244
|
return
|
|
1238
1245
|
}
|
|
1239
|
-
|
|
1246
|
+
const sessions = this.ctx.get?.('sessions') || this.ctx.sessions
|
|
1247
|
+
await sessions?.flush?.(chat.agent.session)
|
|
1240
1248
|
if (progress) try { await progress.remove() } catch {}
|
|
1241
1249
|
progress = null
|
|
1242
1250
|
if (typeof react === 'function') react('').catch?.(() => {})
|
|
@@ -1353,6 +1361,26 @@ export class Gateway {
|
|
|
1353
1361
|
return this.pairing.rejectCode(code)
|
|
1354
1362
|
}
|
|
1355
1363
|
|
|
1364
|
+
async probeTelegram(timeoutMs = 10000) {
|
|
1365
|
+
const adapter = this.getAdapter('telegram')
|
|
1366
|
+
if (!adapter) {
|
|
1367
|
+
return { ok: false, error: 'Telegram adapter not initialized' }
|
|
1368
|
+
}
|
|
1369
|
+
if (typeof adapter.probeHealth === 'function') {
|
|
1370
|
+
return adapter.probeHealth(timeoutMs)
|
|
1371
|
+
}
|
|
1372
|
+
return { ok: false, error: 'probeHealth not implemented on adapter' }
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
getBotInfo() {
|
|
1376
|
+
const adapter = this.getAdapter('telegram')
|
|
1377
|
+
return {
|
|
1378
|
+
botId: adapter?.botId || 0,
|
|
1379
|
+
botUsername: adapter?.botUsername || '',
|
|
1380
|
+
pollingConflict: Boolean(adapter?.pollingConflict),
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1356
1384
|
async messengerAskFromAgent(agent, payload, timeoutMs) {
|
|
1357
1385
|
const sessionId = String(agent?.session?.id || '')
|
|
1358
1386
|
const key = this.sessionToChat.get(sessionId)
|
|
@@ -1372,6 +1400,8 @@ export class Gateway {
|
|
|
1372
1400
|
send: (target, payload) => this.messengerSend(target, payload),
|
|
1373
1401
|
ask: (target, payload, timeoutMs) => this.messengerAsk(target, payload, timeoutMs),
|
|
1374
1402
|
progress: (target, payload) => this.messengerProgress(target, payload),
|
|
1403
|
+
probeTelegram: (timeoutMs) => this.probeTelegram(timeoutMs),
|
|
1404
|
+
getBotInfo: () => this.getBotInfo(),
|
|
1375
1405
|
}
|
|
1376
1406
|
}
|
|
1377
1407
|
}
|
package/lib/index.js
CHANGED
|
@@ -78,7 +78,7 @@ function publicConfig(cfg) {
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
function registerMessengerRoute(ctx, getGw, path, action) {
|
|
81
|
-
ctx.effect(() => ctx.webServer.register({
|
|
81
|
+
ctx.effect(() => (ctx.get?.('webServer') || ctx.webServer).register({
|
|
82
82
|
kind: 'exact', path,
|
|
83
83
|
handler: async (req, res) => {
|
|
84
84
|
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
|
|
@@ -112,6 +112,7 @@ export function apply(ctx, config) {
|
|
|
112
112
|
let source = () => entry
|
|
113
113
|
let settingsApi
|
|
114
114
|
const turnStarts = new Map()
|
|
115
|
+
const getWebServer = () => ctx.get?.('webServer') || ctx.webServer
|
|
115
116
|
|
|
116
117
|
const sync = () => {
|
|
117
118
|
if (gateway) { gateway.stop(); gateway = undefined }
|
|
@@ -154,8 +155,9 @@ export function apply(ctx, config) {
|
|
|
154
155
|
gateway.start().catch((err) => ctx.logger?.warn?.(`dsh-messenger-gateway: ${err.message}`))
|
|
155
156
|
}
|
|
156
157
|
|
|
157
|
-
|
|
158
|
-
|
|
158
|
+
const settings = ctx.get?.('settings') || ctx.settings
|
|
159
|
+
if (settings?.register) {
|
|
160
|
+
settingsApi = settings.register(SETTINGS_NAMESPACE, Config, { base: config || {} })
|
|
159
161
|
source = () => resolveConfig(settingsApi.get() ?? config ?? {})
|
|
160
162
|
ctx.effect(() => settingsApi.watch(sync), 'dsh-messenger-gateway: settings')
|
|
161
163
|
}
|
|
@@ -165,8 +167,9 @@ export function apply(ctx, config) {
|
|
|
165
167
|
ctx.effect(() => ctx.provide('messenger', createMessengerService(getGw)), 'dsh-messenger-gateway: messenger service')
|
|
166
168
|
|
|
167
169
|
// Agent tool: inline buttons in the telegram chat bound to this msgw session
|
|
168
|
-
|
|
169
|
-
|
|
170
|
+
const tools = ctx.get?.('tools') || ctx.tools
|
|
171
|
+
if (tools?.register) {
|
|
172
|
+
ctx.effect(() => tools.register(defineTool({
|
|
170
173
|
name: 'messenger_ask',
|
|
171
174
|
description:
|
|
172
175
|
'Ask the Telegram user a single or multiple-choice question with inline buttons/checkboxes and wait for their choice. '
|
|
@@ -267,11 +270,13 @@ export function apply(ctx, config) {
|
|
|
267
270
|
.catch((e) => ctx.logger?.warn?.(`notify bridge: ${e.message}`))
|
|
268
271
|
}), 'dsh-messenger-gateway: notify bridge')
|
|
269
272
|
|
|
270
|
-
ctx.effect(() =>
|
|
273
|
+
ctx.effect(() => getWebServer().register({
|
|
271
274
|
kind: 'exact', path: '/dsh-messenger-gateway/status',
|
|
272
275
|
handler: async (req, res) => {
|
|
273
276
|
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET only' })
|
|
274
277
|
const gw = getGw()
|
|
278
|
+
const botInfo = gw?.getBotInfo?.() || {}
|
|
279
|
+
const uptimeSec = gw?.stats?.startedAt ? Math.max(0, Math.round((Date.now() - gw.stats.startedAt) / 1000)) : 0
|
|
275
280
|
writeJson(res, 200, {
|
|
276
281
|
ok: true,
|
|
277
282
|
running: Boolean(gw),
|
|
@@ -279,15 +284,44 @@ export function apply(ctx, config) {
|
|
|
279
284
|
activeChats: gw?.messenger.activeChats() || 0,
|
|
280
285
|
ttsEnabled: Boolean(source().tts?.enabled),
|
|
281
286
|
messengerService: 'messenger',
|
|
282
|
-
home:
|
|
283
|
-
homes:
|
|
287
|
+
home: gw?.messenger.home?.() || null,
|
|
288
|
+
homes: gw?.messenger.homes?.() || [],
|
|
284
289
|
pairingPending: gw?.messenger.pairingPending?.()?.length || 0,
|
|
290
|
+
botId: botInfo.botId || 0,
|
|
291
|
+
botUsername: botInfo.botUsername || '',
|
|
292
|
+
pollingConflict: Boolean(botInfo.pollingConflict),
|
|
293
|
+
uptimeSec,
|
|
294
|
+
stats: {
|
|
295
|
+
sent: gw?.stats?.sent || 0,
|
|
296
|
+
errors: gw?.stats?.errors || 0,
|
|
297
|
+
startedAt: gw?.stats?.startedAt || 0,
|
|
298
|
+
},
|
|
285
299
|
config: publicConfig(source()),
|
|
286
300
|
})
|
|
287
301
|
},
|
|
288
302
|
}), 'dsh-messenger-gateway: status')
|
|
289
303
|
|
|
290
|
-
ctx.effect(() =>
|
|
304
|
+
ctx.effect(() => getWebServer().register({
|
|
305
|
+
kind: 'exact', path: '/dsh-messenger-gateway/smoke',
|
|
306
|
+
handler: async (req, res) => {
|
|
307
|
+
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
|
|
308
|
+
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'forbidden' })
|
|
309
|
+
const gw = getGw()
|
|
310
|
+
if (!gw) return writeJson(res, 503, { ok: false, error: 'gateway not running' })
|
|
311
|
+
let payload = {}
|
|
312
|
+
try {
|
|
313
|
+
const raw = (await readBody(req)).toString('utf8').trim()
|
|
314
|
+
if (raw) payload = JSON.parse(raw)
|
|
315
|
+
} catch {
|
|
316
|
+
return writeJson(res, 400, { ok: false, error: 'invalid json' })
|
|
317
|
+
}
|
|
318
|
+
const timeoutMs = Number(payload.timeoutMs) || 10000
|
|
319
|
+
const result = await gw.probeTelegram(timeoutMs)
|
|
320
|
+
writeJson(res, result.ok ? 200 : 502, result)
|
|
321
|
+
},
|
|
322
|
+
}), 'dsh-messenger-gateway: smoke test')
|
|
323
|
+
|
|
324
|
+
ctx.effect(() => getWebServer().register({
|
|
291
325
|
kind: 'exact', path: '/dsh-messenger-gateway/messenger',
|
|
292
326
|
handler: async (req, res) => {
|
|
293
327
|
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET only' })
|
|
@@ -295,7 +329,7 @@ export function apply(ctx, config) {
|
|
|
295
329
|
},
|
|
296
330
|
}), 'dsh-messenger-gateway: messenger schema')
|
|
297
331
|
|
|
298
|
-
ctx.effect(() =>
|
|
332
|
+
ctx.effect(() => getWebServer().register({
|
|
299
333
|
kind: 'exact', path: '/dsh-messenger-gateway/events',
|
|
300
334
|
handler: async (req, res) => {
|
|
301
335
|
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
|
|
@@ -334,7 +368,7 @@ export function apply(ctx, config) {
|
|
|
334
368
|
},
|
|
335
369
|
}), 'dsh-messenger-gateway: webhook events')
|
|
336
370
|
|
|
337
|
-
ctx.effect(() =>
|
|
371
|
+
ctx.effect(() => getWebServer().register({
|
|
338
372
|
kind: 'exact', path: '/dsh-messenger-gateway/config',
|
|
339
373
|
handler: async (req, res) => {
|
|
340
374
|
if (req.method === 'GET') return writeJson(res, 200, { ok: true, config: publicConfig(source()) })
|
|
@@ -370,7 +404,7 @@ export function apply(ctx, config) {
|
|
|
370
404
|
},
|
|
371
405
|
}), 'dsh-messenger-gateway: config')
|
|
372
406
|
|
|
373
|
-
ctx.effect(() =>
|
|
407
|
+
ctx.effect(() => getWebServer().register({
|
|
374
408
|
kind: 'exact', path: '/dsh-messenger-gateway/pairing',
|
|
375
409
|
handler: async (req, res) => {
|
|
376
410
|
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET only' })
|
|
@@ -385,7 +419,7 @@ export function apply(ctx, config) {
|
|
|
385
419
|
},
|
|
386
420
|
}), 'dsh-messenger-gateway: pairing list')
|
|
387
421
|
|
|
388
|
-
ctx.effect(() =>
|
|
422
|
+
ctx.effect(() => getWebServer().register({
|
|
389
423
|
kind: 'exact', path: '/dsh-messenger-gateway/pairing/approve',
|
|
390
424
|
handler: async (req, res) => {
|
|
391
425
|
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
|
|
@@ -399,7 +433,7 @@ export function apply(ctx, config) {
|
|
|
399
433
|
},
|
|
400
434
|
}), 'dsh-messenger-gateway: pairing approve')
|
|
401
435
|
|
|
402
|
-
ctx.effect(() =>
|
|
436
|
+
ctx.effect(() => getWebServer().register({
|
|
403
437
|
kind: 'exact', path: '/dsh-messenger-gateway/pairing/reject',
|
|
404
438
|
handler: async (req, res) => {
|
|
405
439
|
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
|
|
@@ -418,7 +452,7 @@ export function apply(ctx, config) {
|
|
|
418
452
|
return p || '/dsh-messenger-gateway/telegram/webhook'
|
|
419
453
|
}
|
|
420
454
|
|
|
421
|
-
ctx.effect(() =>
|
|
455
|
+
ctx.effect(() => getWebServer().register({
|
|
422
456
|
kind: 'exact', path: webhookPath(),
|
|
423
457
|
handler: async (req, res) => {
|
|
424
458
|
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
|
package/lib/models.js
CHANGED
|
@@ -7,14 +7,15 @@ export async function listModelCatalog(ctx, fallback = {}) {
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
// 1. Try ctx.llm if available
|
|
10
|
-
|
|
10
|
+
const llm = ctx?.get?.('llm') || ctx?.llm
|
|
11
|
+
if (llm?.listProviders) {
|
|
11
12
|
try {
|
|
12
|
-
const providers = await
|
|
13
|
+
const providers = await llm.listProviders()
|
|
13
14
|
for (const p of providers || []) {
|
|
14
15
|
const pId = p.id || p
|
|
15
16
|
result.providers.push({ id: pId, name: p.name || pId })
|
|
16
17
|
try {
|
|
17
|
-
const models = await
|
|
18
|
+
const models = await llm.listModels(pId)
|
|
18
19
|
result.modelsByProvider.set(
|
|
19
20
|
pId,
|
|
20
21
|
(models || []).map((m) => ({ id: m.id || m, name: m.name || m.id || m }))
|
package/lib/outbound.js
CHANGED
|
@@ -62,7 +62,8 @@ export function fileNameForRef(ref) {
|
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
export async function readAttachmentBytes(ctx, ref) {
|
|
65
|
-
const
|
|
65
|
+
const attachments = ctx.get?.('attachments') || ctx.attachments
|
|
66
|
+
const stored = await attachments.readImage(ref)
|
|
66
67
|
return {
|
|
67
68
|
bytes: stored.data,
|
|
68
69
|
mime: ref.mediaType || stored.ref?.mediaType || 'image/png',
|
package/lib/photos.js
CHANGED
|
@@ -12,7 +12,8 @@ export async function attachInboundPhoto(ctx, att, opts = {}) {
|
|
|
12
12
|
if (maxBytes > 0 && bytes.length > maxBytes) {
|
|
13
13
|
throw new Error(`image too large (${bytes.length} bytes, max ${maxBytes})`)
|
|
14
14
|
}
|
|
15
|
-
const
|
|
15
|
+
const attachments = ctx.get?.('attachments') || ctx.attachments
|
|
16
|
+
const ref = await attachments.saveImage({
|
|
16
17
|
data: bytes,
|
|
17
18
|
mediaType: att.mime || 'image/jpeg',
|
|
18
19
|
...(att.name ? { name: att.name } : {}),
|
package/lib/session-ops.js
CHANGED
|
@@ -87,9 +87,19 @@ export function rewindSession(session, turnsCount = 1) {
|
|
|
87
87
|
}
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
const
|
|
90
|
+
const keepCount = Math.max(0, messages.length - toRemove)
|
|
91
|
+
const kept = messages.slice(0, keepCount).map(m => {
|
|
92
|
+
// Only wrap non-string invalid/nullish/object content to array
|
|
93
|
+
if (m && m.content && typeof m.content === 'object' && !Array.isArray(m.content) && m.content.type) {
|
|
94
|
+
return { ...m, content: [m.content] }
|
|
95
|
+
}
|
|
96
|
+
return m
|
|
97
|
+
})
|
|
98
|
+
const removedCount = messages.length - keepCount
|
|
99
|
+
session.messages = kept
|
|
100
|
+
|
|
91
101
|
return {
|
|
92
|
-
removed:
|
|
93
|
-
remaining: messages.length,
|
|
102
|
+
removed: removedCount,
|
|
103
|
+
remaining: session.messages.length,
|
|
94
104
|
}
|
|
95
|
-
}
|
|
105
|
+
}
|
package/lib/telegram-errors.js
CHANGED
|
@@ -1,41 +1,51 @@
|
|
|
1
|
-
// Network error classification for Telegram sends (mirrors Hermes adapter policy).
|
|
2
|
-
//
|
|
3
|
-
// The key distinction: some network failures mean the request NEVER left the
|
|
4
|
-
// process (connect/pool timeout, ECONNRESET before send) → resending is safe.
|
|
5
|
-
// Others (a generic timeout after the request may have reached Telegram) could
|
|
6
|
-
// duplicate a message if we resend → do NOT retry.
|
|
7
|
-
|
|
8
|
-
function rootCause(err) {
|
|
9
|
-
let e = err
|
|
10
|
-
let depth = 0
|
|
11
|
-
while (e && e.cause && e.cause !== e && depth < 10) {
|
|
12
|
-
e = e.cause
|
|
13
|
-
depth++
|
|
14
|
-
}
|
|
15
|
-
return e || err
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// True when a resend cannot duplicate a message: the request did not reach Telegram.
|
|
19
|
-
export function isResendSafeNetworkError(err) {
|
|
20
|
-
const c = rootCause(err)
|
|
21
|
-
const msg = String(c?.message || err?.message || '').toLowerCase()
|
|
22
|
-
if (/not sent to telegram|connect timeout|und_err_connect|econnreset|enotfound|econnrefused|ECONNRESET|ENOTFOUND|ECONNREFUSED/i.test(msg)) {
|
|
23
|
-
return true
|
|
24
|
-
}
|
|
25
|
-
// undici PoolTimeout message: "Request was *not* sent to Telegram."
|
|
26
|
-
if (/pool timeout|request was \*?not\*? sent/i.test(msg)) return true
|
|
27
|
-
return false
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
// True for a 409 from getUpdates: a second bot instance polls the same token.
|
|
31
|
-
export function isPollingConflict(err) {
|
|
32
|
-
const msg = String(err?.message || '').toLowerCase()
|
|
33
|
-
return /terminated by other getupdates request|another bot instance is running|getupdates.*conflict|conflict.*getupdates/i.test(msg)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// True when the target chat/topic no longer exists (deleted/closed/upgraded).
|
|
37
|
-
// Such sends should not be retried and any pending ask binding must be pruned.
|
|
38
|
-
export function isTopicGoneError(err) {
|
|
39
|
-
const msg = String(err?.message || '').toLowerCase()
|
|
40
|
-
return /thread not found|message thread not found|topic[_ ]?(not found|closed|deleted)|chat (not found|was (upgraded|deleted))|group chat was (upgraded|deleted)/i.test(msg)
|
|
41
|
-
}
|
|
1
|
+
// Network error classification for Telegram sends (mirrors Hermes adapter policy).
|
|
2
|
+
//
|
|
3
|
+
// The key distinction: some network failures mean the request NEVER left the
|
|
4
|
+
// process (connect/pool timeout, ECONNRESET before send) → resending is safe.
|
|
5
|
+
// Others (a generic timeout after the request may have reached Telegram) could
|
|
6
|
+
// duplicate a message if we resend → do NOT retry.
|
|
7
|
+
|
|
8
|
+
function rootCause(err) {
|
|
9
|
+
let e = err
|
|
10
|
+
let depth = 0
|
|
11
|
+
while (e && e.cause && e.cause !== e && depth < 10) {
|
|
12
|
+
e = e.cause
|
|
13
|
+
depth++
|
|
14
|
+
}
|
|
15
|
+
return e || err
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// True when a resend cannot duplicate a message: the request did not reach Telegram.
|
|
19
|
+
export function isResendSafeNetworkError(err) {
|
|
20
|
+
const c = rootCause(err)
|
|
21
|
+
const msg = String(c?.message || err?.message || '').toLowerCase()
|
|
22
|
+
if (/not sent to telegram|connect timeout|und_err_connect|econnreset|enotfound|econnrefused|ECONNRESET|ENOTFOUND|ECONNREFUSED/i.test(msg)) {
|
|
23
|
+
return true
|
|
24
|
+
}
|
|
25
|
+
// undici PoolTimeout message: "Request was *not* sent to Telegram."
|
|
26
|
+
if (/pool timeout|request was \*?not\*? sent/i.test(msg)) return true
|
|
27
|
+
return false
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// True for a 409 from getUpdates: a second bot instance polls the same token.
|
|
31
|
+
export function isPollingConflict(err) {
|
|
32
|
+
const msg = String(err?.message || '').toLowerCase()
|
|
33
|
+
return /terminated by other getupdates request|another bot instance is running|getupdates.*conflict|conflict.*getupdates/i.test(msg)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// True when the target chat/topic no longer exists (deleted/closed/upgraded).
|
|
37
|
+
// Such sends should not be retried and any pending ask binding must be pruned.
|
|
38
|
+
export function isTopicGoneError(err) {
|
|
39
|
+
const msg = String(err?.message || '').toLowerCase()
|
|
40
|
+
return /thread not found|message thread not found|topic[_ ]?(not found|closed|deleted)|chat (not found|was (upgraded|deleted))|group chat was (upgraded|deleted)/i.test(msg)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Calculates exponential backoff with max cap for polling retries.
|
|
45
|
+
* errorCount is 1-indexed.
|
|
46
|
+
*/
|
|
47
|
+
export function computePollBackoffMs(baseIntervalMs, errorCount, maxDelayMs = 30_000) {
|
|
48
|
+
const count = Math.max(1, Math.min(10, Number(errorCount) || 1))
|
|
49
|
+
const base = Math.max(100, Number(baseIntervalMs) || 500)
|
|
50
|
+
return Math.min(maxDelayMs, base * Math.pow(2, count - 1))
|
|
51
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-messenger-gateway",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.18",
|
|
4
4
|
"description": "Telegram messenger bridge for DeepSeek Harness: sessions, steer, homes, inline asks, notify bridge, and optional TTS voice notes.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -42,10 +42,7 @@
|
|
|
42
42
|
},
|
|
43
43
|
"client": {
|
|
44
44
|
"platform": "web",
|
|
45
|
-
"inject": [
|
|
46
|
-
"@deepseek-ai/dsh-client-locale",
|
|
47
|
-
"@deepseek-ai/dsh-client-ui-settings"
|
|
48
|
-
]
|
|
45
|
+
"inject": []
|
|
49
46
|
}
|
|
50
47
|
},
|
|
51
48
|
"peerDependencies": {
|