@goodandready/dsh-messenger-gateway 0.3.10 → 0.3.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/adapters/telegram.js +26 -4
- package/lib/artifacts.js +1 -1
- package/lib/file-manager.js +3 -2
- package/lib/gateway.js +11 -15
- package/lib/models.js +14 -1
- package/lib/scheduler.js +8 -0
- package/lib/telegram-format.js +2 -2
- package/package.json +1 -1
package/lib/adapters/telegram.js
CHANGED
|
@@ -69,8 +69,12 @@ export class TelegramAdapter {
|
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
async call(method, params = {}) {
|
|
72
|
+
const timeoutMs = (this.timeoutSeconds * 1000) + 15000
|
|
72
73
|
const res = await fetch(`${API}/bot${this.token}/${method}`, {
|
|
73
|
-
method: 'POST',
|
|
74
|
+
method: 'POST',
|
|
75
|
+
headers: { 'Content-Type': 'application/json' },
|
|
76
|
+
body: JSON.stringify(params),
|
|
77
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
74
78
|
})
|
|
75
79
|
const json = await res.json().catch(() => ({}))
|
|
76
80
|
if (!res.ok || json.ok === false) throw new Error(`telegram ${method}: ${json.description || res.status}`)
|
|
@@ -78,7 +82,12 @@ export class TelegramAdapter {
|
|
|
78
82
|
}
|
|
79
83
|
|
|
80
84
|
async callMultipart(method, form) {
|
|
81
|
-
const
|
|
85
|
+
const timeoutMs = (this.timeoutSeconds * 1000) + 30000
|
|
86
|
+
const res = await fetch(`${API}/bot${this.token}/${method}`, {
|
|
87
|
+
method: 'POST',
|
|
88
|
+
body: form,
|
|
89
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
90
|
+
})
|
|
82
91
|
const json = await res.json().catch(() => ({}))
|
|
83
92
|
if (!res.ok || json.ok === false) throw new Error(`telegram ${method}: ${json.description || res.status}`)
|
|
84
93
|
return json.result
|
|
@@ -105,7 +114,10 @@ export class TelegramAdapter {
|
|
|
105
114
|
async getFile(fileId) { return this.call('getFile', { file_id: fileId }) }
|
|
106
115
|
|
|
107
116
|
async downloadFile(filePath) {
|
|
108
|
-
const
|
|
117
|
+
const timeoutMs = (this.timeoutSeconds * 1000) + 30000
|
|
118
|
+
const res = await fetch(`${API}/file/bot${this.token}/${filePath}`, {
|
|
119
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
120
|
+
})
|
|
109
121
|
if (!res.ok) throw new Error(`telegram download HTTP ${res.status}`)
|
|
110
122
|
return new Uint8Array(await res.arrayBuffer())
|
|
111
123
|
}
|
|
@@ -203,6 +215,15 @@ export class TelegramAdapter {
|
|
|
203
215
|
|
|
204
216
|
async dispatchUpdate(update) {
|
|
205
217
|
if (update.callback_query) {
|
|
218
|
+
const fromId = update.callback_query.from?.id
|
|
219
|
+
if (fromId && !this.allowed(fromId)) {
|
|
220
|
+
await this.call('answerCallbackQuery', {
|
|
221
|
+
callback_query_id: update.callback_query.id,
|
|
222
|
+
text: 'Нет доступа',
|
|
223
|
+
show_alert: true,
|
|
224
|
+
}).catch(() => {})
|
|
225
|
+
return
|
|
226
|
+
}
|
|
206
227
|
try { await this.onCallback?.(this.wrapCallback(update.callback_query)) } catch (e) {
|
|
207
228
|
this.logger?.warn?.(`callback: ${e.message}`)
|
|
208
229
|
}
|
|
@@ -489,7 +510,8 @@ export class TelegramAdapter {
|
|
|
489
510
|
if (thread.message_thread_id) form.append('message_thread_id', String(thread.message_thread_id))
|
|
490
511
|
const blob = new Blob([file.bytes])
|
|
491
512
|
const name = safeName(file.name || 'file')
|
|
492
|
-
|
|
513
|
+
const isSvg = file.mime === 'image/svg+xml' || (file.name && file.name.toLowerCase().endsWith('.svg'))
|
|
514
|
+
if (file.kind === 'photo' && !isSvg) { form.append('photo', blob, name); return this.sendWithRetry('sendPhoto', form, { multipart: true }) }
|
|
493
515
|
if (file.kind === 'voice') { form.append('voice', blob, name); return this.sendWithRetry('sendVoice', form, { multipart: true }) }
|
|
494
516
|
if (file.kind === 'audio') { form.append('audio', blob, name); return this.sendWithRetry('sendAudio', form, { multipart: true }) }
|
|
495
517
|
if (file.kind === 'video') { form.append('video', blob, name); return this.sendWithRetry('sendVideo', form, { multipart: true }) }
|
package/lib/artifacts.js
CHANGED
|
@@ -102,7 +102,7 @@ export function processDiagramsAndTables(rawText, options = {}) {
|
|
|
102
102
|
files.push({
|
|
103
103
|
name: `diagram-${diagramIndex}.svg`,
|
|
104
104
|
mime: 'image/svg+xml',
|
|
105
|
-
kind: '
|
|
105
|
+
kind: 'document',
|
|
106
106
|
dataBase64: base64,
|
|
107
107
|
bytes: Buffer.from(svgContent, 'utf-8'),
|
|
108
108
|
})
|
package/lib/file-manager.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readdir, stat, readFile } from 'node:fs/promises'
|
|
2
|
-
import { resolve, normalize, relative, join, extname } from 'node:path'
|
|
2
|
+
import { resolve, normalize, relative, join, extname, isAbsolute } from 'node:path'
|
|
3
3
|
|
|
4
4
|
export function formatFileSize(bytes) {
|
|
5
5
|
if (!Number.isFinite(bytes) || bytes <= 0) return '0 B'
|
|
@@ -14,8 +14,9 @@ export function resolveSafePath(baseCwd, userPath = '.') {
|
|
|
14
14
|
const cleanUser = String(userPath || '').trim() || '.'
|
|
15
15
|
const target = resolve(base, cleanUser)
|
|
16
16
|
|
|
17
|
+
const isDrivePath = /^[a-zA-Z]:/.test(cleanUser)
|
|
17
18
|
const rel = relative(base, target)
|
|
18
|
-
if (rel.startsWith('..') || (rel && resolve(base, rel) !== target)) {
|
|
19
|
+
if (isAbsolute(cleanUser) || isDrivePath || rel.startsWith('..') || isAbsolute(rel) || (rel && resolve(base, rel) !== target)) {
|
|
19
20
|
const err = new Error('Access denied: path traversal out of workspace')
|
|
20
21
|
err.code = 'ERR_PATH_TRAVERSAL'
|
|
21
22
|
throw err
|
package/lib/gateway.js
CHANGED
|
@@ -296,6 +296,10 @@ export class Gateway {
|
|
|
296
296
|
}
|
|
297
297
|
|
|
298
298
|
async handleCallback(cb) {
|
|
299
|
+
if (cb.userId && !this.isUserAllowed(cb.userId)) {
|
|
300
|
+
try { await cb.answer('Нет доступа.') } catch {}
|
|
301
|
+
return
|
|
302
|
+
}
|
|
299
303
|
const indexed = this.callbackIndex.get(cb.data)
|
|
300
304
|
const { token, buttonId } = parseCallbackData(cb.data)
|
|
301
305
|
const askToken = indexed || token
|
|
@@ -368,7 +372,7 @@ export class Gateway {
|
|
|
368
372
|
const sub = parts[1]
|
|
369
373
|
// Step 2: Selected provider -> show its models (10 per page)
|
|
370
374
|
if (sub === 'p') {
|
|
371
|
-
const providerId = parts
|
|
375
|
+
const providerId = parts.slice(2).join(':')
|
|
372
376
|
const current = this.resolveAgentModel()
|
|
373
377
|
const catalog = await listModelCatalog(this.ctx, current)
|
|
374
378
|
const models = catalog.modelsByProvider.get(providerId) || []
|
|
@@ -389,8 +393,8 @@ export class Gateway {
|
|
|
389
393
|
}
|
|
390
394
|
// Pagination for models
|
|
391
395
|
if (sub === 'pg') {
|
|
392
|
-
const
|
|
393
|
-
const
|
|
396
|
+
const page = parseInt(parts[parts.length - 1], 10) || 0
|
|
397
|
+
const providerId = parts.slice(2, -1).join(':')
|
|
394
398
|
const current = this.resolveAgentModel()
|
|
395
399
|
const catalog = await listModelCatalog(this.ctx, current)
|
|
396
400
|
const models = catalog.modelsByProvider.get(providerId) || []
|
|
@@ -709,16 +713,6 @@ export class Gateway {
|
|
|
709
713
|
}
|
|
710
714
|
return reply({ text: `📄 Файл: <b>${res.name}</b> (${formatFileSize(res.size)})`, files: [file] })
|
|
711
715
|
}
|
|
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
|
-
}
|
|
722
716
|
if (cmd === '/new') {
|
|
723
717
|
const chat = this.chats.get(key)
|
|
724
718
|
if (chat) {
|
|
@@ -1328,8 +1322,10 @@ export class Gateway {
|
|
|
1328
1322
|
if (timeout <= 0) return
|
|
1329
1323
|
const now = Date.now()
|
|
1330
1324
|
for (const [key, chat] of this.chats) {
|
|
1331
|
-
if (now - chat.lastUsed > timeout) {
|
|
1332
|
-
|
|
1325
|
+
if (!chat.turnActive && now - chat.lastUsed > timeout) {
|
|
1326
|
+
if (chat.agent?.session?.id) {
|
|
1327
|
+
this.sessionToChat.delete(String(chat.agent.session.id))
|
|
1328
|
+
}
|
|
1333
1329
|
this.chats.delete(key)
|
|
1334
1330
|
chat.dispose().catch(() => {})
|
|
1335
1331
|
}
|
package/lib/models.js
CHANGED
|
@@ -39,16 +39,29 @@ export async function listModelCatalog(ctx, fallback = {}) {
|
|
|
39
39
|
|
|
40
40
|
// In-memory registry for model picker callback tokens to avoid 64-byte Telegram limit
|
|
41
41
|
const modelIndexStore = new Map()
|
|
42
|
+
const modelToKeyMap = new Map()
|
|
42
43
|
let modelIndexCounter = 0
|
|
43
44
|
|
|
44
45
|
export function storeModelSelection(provider, model) {
|
|
46
|
+
const compositeKey = `${provider}::${model}`
|
|
47
|
+
const existingKey = modelToKeyMap.get(compositeKey)
|
|
48
|
+
if (existingKey && modelIndexStore.has(existingKey)) {
|
|
49
|
+
modelIndexStore.get(existingKey).time = Date.now()
|
|
50
|
+
return existingKey
|
|
51
|
+
}
|
|
52
|
+
|
|
45
53
|
const key = String(++modelIndexCounter)
|
|
46
54
|
modelIndexStore.set(key, { provider, model, time: Date.now() })
|
|
55
|
+
modelToKeyMap.set(compositeKey, key)
|
|
56
|
+
|
|
47
57
|
// Cleanup entries older than 1 hour
|
|
48
58
|
if (modelIndexStore.size > 200) {
|
|
49
59
|
const cutoff = Date.now() - 3600000
|
|
50
60
|
for (const [k, v] of modelIndexStore.entries()) {
|
|
51
|
-
if (v.time < cutoff)
|
|
61
|
+
if (v.time < cutoff) {
|
|
62
|
+
modelIndexStore.delete(k)
|
|
63
|
+
modelToKeyMap.delete(`${v.provider}::${v.model}`)
|
|
64
|
+
}
|
|
52
65
|
}
|
|
53
66
|
}
|
|
54
67
|
return key
|
package/lib/scheduler.js
CHANGED
|
@@ -46,8 +46,16 @@ export function createScheduler(filePath, onDue) {
|
|
|
46
46
|
loaded = true
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
const RETENTION_MS = 7 * 86400 * 1000
|
|
50
|
+
|
|
49
51
|
async function save() {
|
|
50
52
|
try {
|
|
53
|
+
const now = Date.now()
|
|
54
|
+
tasks = tasks.filter((t) => {
|
|
55
|
+
if (t.status === 'pending') return true
|
|
56
|
+
const finishTime = t.firedAt || t.createdAt || 0
|
|
57
|
+
return (now - finishTime) < RETENTION_MS
|
|
58
|
+
})
|
|
51
59
|
await mkdir(dirname(filePath), { recursive: true })
|
|
52
60
|
await writeFile(filePath, JSON.stringify(tasks, null, 2), 'utf8')
|
|
53
61
|
} catch {}
|
package/lib/telegram-format.js
CHANGED
|
@@ -12,7 +12,7 @@ function renderInline(t) {
|
|
|
12
12
|
let s = escapeHtml(t)
|
|
13
13
|
s = s.replace(/\*\*([^*\n]+)\*\*/g, '<b>$1</b>')
|
|
14
14
|
s = s.replace(/`([^`\n]+)`/g, '<code>$1</code>')
|
|
15
|
-
s = s.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_, label, url) => `<a href="${
|
|
15
|
+
s = s.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_, label, url) => `<a href="${url.replace(/"/g, '%22')}">${label}</a>`)
|
|
16
16
|
s = s.replace(/(^|[^*])\*([^*\n]+)\*(?!\*)/g, '$1<i>$2</i>')
|
|
17
17
|
return s
|
|
18
18
|
}
|
|
@@ -123,7 +123,7 @@ export function markdownToTelegramHtml(markdown) {
|
|
|
123
123
|
text = convertDetails(text, stash)
|
|
124
124
|
text = convertTaskLists(text, stash)
|
|
125
125
|
text = escapeHtml(text)
|
|
126
|
-
text = text.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_, label, url) => `<a href="${
|
|
126
|
+
text = text.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_, label, url) => `<a href="${url.replace(/"/g, '%22')}">${label}</a>`)
|
|
127
127
|
text = text.replace(/\*\*([^*\n]+)\*\*/g, '<b>$1</b>')
|
|
128
128
|
text = text.replace(/__([^_\n]+)__/g, '<b>$1</b>')
|
|
129
129
|
text = text.replace(/~~([^~\n]+)~~/g, '<s>$1</s>')
|
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.11",
|
|
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",
|