@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 CHANGED
@@ -1,6 +1,44 @@
1
- # @goodandready/dsh-messenger-gateway
1
+ # 📦 @goodandready/dsh-messenger-gateway
2
2
 
3
- Telegram messenger bridge for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness).
3
+ <div align="center">
4
+
5
+ <h3>Telegram Messenger Bridge with Interactive Buttons, Forum Topics & Voice Notes for DeepSeek Harness</h3>
6
+
7
+ <p align="center">
8
+ <a href="https://www.npmjs.com/package/@goodandready/dsh-messenger-gateway"><img src="https://img.shields.io/npm/v/@goodandready/dsh-messenger-gateway.svg?style=for-the-badge&color=6366f1&labelColor=1e1b4b" alt="npm version"></a>
9
+ <a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-10b981.svg?style=for-the-badge&color=10b981&labelColor=064e3b" alt="license"></a>
10
+ <a href="https://github.com/topics/dsh-plugin"><img src="https://img.shields.io/badge/DSH-Plugin-8b5cf6.svg?style=for-the-badge&labelColor=2e1065" alt="DSH Plugin"></a>
11
+ <a href="https://nodejs.org"><img src="https://img.shields.io/badge/Node-20%2B-f59e0b.svg?style=for-the-badge&labelColor=451a03" alt="Node version"></a>
12
+ </p>
13
+
14
+ <p align="center">
15
+ <a href="https://goodandready.app/"><img src="https://img.shields.io/badge/All_Author_Projects-goodandready.app-ff4500.svg?style=for-the-badge&logo=rocket&logoColor=white&labelColor=1a1a2e" alt="All Author Projects"></a>
16
+ </p>
17
+
18
+ <p align="center">
19
+ <a href="README.md"><b>🇬🇧 English</b></a> •
20
+ <a href="README.ru.md"><b>🇷🇺 Русский</b></a> •
21
+ <a href="README.zh.md"><b>🇨🇳 中文说明</b></a>
22
+ </p>
23
+
24
+ <table align="center">
25
+ <tr>
26
+ <td align="center">
27
+ ⭐ <strong>If you like this plugin, please star it on GitHub</strong> — it shows me that the plugin is useful to you and motivates me to keep developing it.
28
+ <br><br>
29
+ 🐛 <strong>If you find a bug or would like to request a feature</strong>, open a GitHub issue in any language — I will review your proposal and implement useful suggestions in a future plugin version.
30
+ </td>
31
+ </tr>
32
+ </table>
33
+
34
+ </div>
35
+
36
+ ---
37
+
38
+ ## ⚡ Overview
39
+
40
+ **`dsh-messenger-gateway`** provides an enterprise-grade, multi-transport messaging gateway for **DeepSeek Harness** agents.
41
+ Talk to your Harness agent directly from Telegram: interactive keyboard buttons, forum topic sessions, private user workspaces, pairing codes, and optional spoken voice replies.
4
42
 
5
43
  Talk to your Harness agent from Telegram: text, voice, photos, documents, inline buttons, named homes, and optional spoken replies.
6
44
 
package/README.ru.md CHANGED
@@ -21,6 +21,16 @@
21
21
  <a href="README.zh.md"><b>🇨🇳 中文说明</b></a>
22
22
  </p>
23
23
 
24
+ <table align="center">
25
+ <tr>
26
+ <td align="center">
27
+ ⭐ <strong>Если вам нравится этот плагин, поставьте ему звезду на GitHub</strong> — это покажет мне, что плагин вам полезен, и будет мотивировать меня развивать его дальше.
28
+ <br><br>
29
+ 🐛 <strong>Если вы нашли баг или хотите предложить новый функционал</strong>, создайте issue на GitHub на любом языке — я рассмотрю ваше предложение и реализую полезные идеи в одной из следующих версий плагина.
30
+ </td>
31
+ </tr>
32
+ </table>
33
+
24
34
  </div>
25
35
 
26
36
  ---
package/README.zh.md CHANGED
@@ -21,6 +21,16 @@
21
21
  <a href="README.zh.md"><b>🇨🇳 中文说明</b></a>
22
22
  </p>
23
23
 
24
+ <table align="center">
25
+ <tr>
26
+ <td align="center">
27
+ ⭐ <strong>如果您喜欢这个插件,请在 GitHub 上为它点亮 Star</strong> — 这能让我知道插件对您有用,并鼓励我继续开发和维护它。
28
+ <br><br>
29
+ 🐛 <strong>如果您发现 Bug 或希望增加功能</strong>,请使用任意语言在 GitHub 上提交 Issue — 我会评估您的建议,并在后续版本中实现有价值的改进。
30
+ </td>
31
+ </tr>
32
+ </table>
33
+
24
34
  </div>
25
35
 
26
36
  ---
@@ -20,6 +20,30 @@ export function splitDiscordText(text, limit = DISCORD_MAX_LENGTH) {
20
20
  return chunks
21
21
  }
22
22
 
23
+ export function toDiscordComponents(buttons) {
24
+ if (!buttons || !Array.isArray(buttons) || !buttons.length) return undefined
25
+ const rows = []
26
+ const grid = Array.isArray(buttons[0]) ? buttons : [buttons]
27
+ for (const row of grid.slice(0, 5)) {
28
+ const components = []
29
+ for (const btn of (row || []).slice(0, 5)) {
30
+ if (!btn) continue
31
+ const custom_id = String(btn.id || btn.callback_data || btn.text || 'btn').slice(0, 100)
32
+ const label = String(btn.text || btn.label || 'Action').slice(0, 80)
33
+ components.push({
34
+ type: 2, // Button
35
+ style: 1, // Primary
36
+ label,
37
+ custom_id,
38
+ })
39
+ }
40
+ if (components.length) {
41
+ rows.push({ type: 1, components })
42
+ }
43
+ }
44
+ return rows.length ? rows : undefined
45
+ }
46
+
23
47
  export class DiscordAdapter {
24
48
  constructor(opts = {}) {
25
49
  this.name = 'discord'
@@ -45,18 +69,21 @@ export class DiscordAdapter {
45
69
  const body = typeof payload === 'string' ? { text: payload } : (payload || {})
46
70
  const text = String(body.text || '')
47
71
  const files = Array.isArray(body.files) ? body.files : []
72
+ const components = toDiscordComponents(body.buttons || body.replyMarkup?.inline_keyboard)
48
73
 
49
74
  const chunks = splitDiscordText(text)
50
- if (!chunks.length && !files.length) return { ok: true }
75
+ if (!chunks.length && !files.length && !components) return { ok: true }
51
76
 
52
77
  // Case 1: Webhook sending
53
78
  const isWebhookTarget = !channelId || channelId === 'default' || channelId === 'webhook'
54
79
  if (this.webhookUrl && isWebhookTarget) {
55
80
  for (const chunk of (chunks.length ? chunks : [''])) {
81
+ const payloadJson = { content: chunk }
82
+ if (components) payloadJson.components = components
56
83
  const res = await fetch(this.webhookUrl, {
57
84
  method: 'POST',
58
85
  headers: { 'Content-Type': 'application/json' },
59
- body: JSON.stringify({ content: chunk }),
86
+ body: JSON.stringify(payloadJson),
60
87
  })
61
88
  if (!res.ok) {
62
89
  const errText = await res.text().catch(() => '')
@@ -77,9 +104,9 @@ export class DiscordAdapter {
77
104
  // Handle files if any on the first chunk
78
105
  if (files.length > 0 && typeof FormData !== 'undefined') {
79
106
  const form = new FormData()
80
- form.append('payload_json', JSON.stringify({
81
- content: chunks[0] || '',
82
- }))
107
+ const payloadJson = { content: chunks[0] || '' }
108
+ if (components) payloadJson.components = components
109
+ form.append('payload_json', JSON.stringify(payloadJson))
83
110
  for (let i = 0; i < files.length; i++) {
84
111
  const file = files[i]
85
112
  const bytes = file.bytes || (file.path ? await readFile(file.path) : null)
@@ -100,32 +127,59 @@ export class DiscordAdapter {
100
127
  const errText = await res.text().catch(() => '')
101
128
  throw new Error(`discord send error ${res.status}: ${errText}`)
102
129
  }
130
+ const json = await res.json().catch(() => ({}))
103
131
  // Send remaining text chunks if any
104
132
  for (let i = 1; i < chunks.length; i++) {
105
133
  await this._sendRestMessage(targetChannel, chunks[i])
106
134
  }
107
- return { ok: true }
135
+ return { ok: true, messageId: json.id }
108
136
  }
109
137
 
110
- // Pure text chunks
111
- for (const chunk of chunks) {
112
- await this._sendRestMessage(targetChannel, chunk)
138
+ // Pure text chunks with components on first chunk
139
+ let firstResult = { ok: true }
140
+ for (let i = 0; i < (chunks.length ? chunks.length : 1); i++) {
141
+ const chunk = chunks[i] || ''
142
+ const res = await this._sendRestMessage(targetChannel, chunk, i === 0 ? components : undefined)
143
+ if (i === 0) firstResult = res
144
+ }
145
+ return firstResult
146
+ }
147
+
148
+ async editMessage(channelId, messageId, content, components) {
149
+ if (!this.botToken) throw new Error('discord botToken required to edit message')
150
+ const body = { content: String(content || '').slice(0, DISCORD_MAX_LENGTH) }
151
+ if (components !== undefined) body.components = components
152
+ const res = await fetch(`${DISCORD_API}/channels/${channelId}/messages/${messageId}`, {
153
+ method: 'PATCH',
154
+ headers: {
155
+ Authorization: `Bot ${this.botToken}`,
156
+ 'Content-Type': 'application/json',
157
+ },
158
+ body: JSON.stringify(body),
159
+ })
160
+ if (!res.ok) {
161
+ const errText = await res.text().catch(() => '')
162
+ throw new Error(`discord edit error ${res.status}: ${errText}`)
113
163
  }
114
164
  return { ok: true }
115
165
  }
116
166
 
117
- async _sendRestMessage(channelId, content) {
167
+ async _sendRestMessage(channelId, content, components) {
168
+ const body = { content }
169
+ if (components && components.length) body.components = components
118
170
  const res = await fetch(`${DISCORD_API}/channels/${channelId}/messages`, {
119
171
  method: 'POST',
120
172
  headers: {
121
173
  Authorization: `Bot ${this.botToken}`,
122
174
  'Content-Type': 'application/json',
123
175
  },
124
- body: JSON.stringify({ content }),
176
+ body: JSON.stringify(body),
125
177
  })
126
178
  if (!res.ok) {
127
179
  const errText = await res.text().catch(() => '')
128
180
  throw new Error(`discord send error ${res.status}: ${errText}`)
129
181
  }
182
+ const json = typeof res.json === 'function' ? await res.json().catch(() => ({})) : {}
183
+ return { ok: true, messageId: json?.id }
130
184
  }
131
185
  }
@@ -17,6 +17,35 @@ export function splitSlackText(text, limit = SLACK_MAX_LENGTH) {
17
17
  return chunks
18
18
  }
19
19
 
20
+ export function toSlackBlocks(text, buttons) {
21
+ const blocks = []
22
+ if (text) {
23
+ blocks.push({
24
+ type: 'section',
25
+ text: { type: 'mrkdwn', text: String(text).slice(0, 3000) },
26
+ })
27
+ }
28
+ if (buttons && Array.isArray(buttons) && buttons.length) {
29
+ const flat = (Array.isArray(buttons[0]) ? buttons.flat() : buttons).slice(0, 25)
30
+ const elements = []
31
+ for (const btn of flat) {
32
+ if (!btn) continue
33
+ const action_id = String(btn.id || btn.callback_data || btn.text || 'btn').slice(0, 100)
34
+ const label = String(btn.text || btn.label || 'Action').slice(0, 75)
35
+ elements.push({
36
+ type: 'button',
37
+ text: { type: 'plain_text', text: label },
38
+ action_id,
39
+ value: action_id,
40
+ })
41
+ }
42
+ if (elements.length) {
43
+ blocks.push({ type: 'actions', elements })
44
+ }
45
+ }
46
+ return blocks.length ? blocks : undefined
47
+ }
48
+
20
49
  export class SlackAdapter {
21
50
  constructor(opts = {}) {
22
51
  this.name = 'slack'
@@ -41,16 +70,19 @@ export class SlackAdapter {
41
70
  if (this.stopped) throw new Error('slack adapter stopped')
42
71
  const body = typeof payload === 'string' ? { text: payload } : (payload || {})
43
72
  const text = String(body.text || '')
73
+ const blocks = toSlackBlocks(text, body.buttons || body.options || body.replyMarkup?.inline_keyboard)
44
74
  const chunks = splitSlackText(text)
45
- if (!chunks.length) return { ok: true }
75
+ if (!chunks.length && !blocks) return { ok: true }
46
76
 
47
77
  const isWebhookTarget = !channelId || channelId === 'default' || channelId === 'webhook'
48
78
  if (this.webhookUrl && isWebhookTarget) {
49
- for (const chunk of chunks) {
79
+ for (const chunk of (chunks.length ? chunks : [''])) {
80
+ const payloadJson = { text: chunk }
81
+ if (blocks) payloadJson.blocks = blocks
50
82
  const res = await fetch(this.webhookUrl, {
51
83
  method: 'POST',
52
84
  headers: { 'Content-Type': 'application/json' },
53
- body: JSON.stringify({ text: chunk }),
85
+ body: JSON.stringify(payloadJson),
54
86
  })
55
87
  if (!res.ok) {
56
88
  const errText = await res.text().catch(() => '')
@@ -68,23 +100,51 @@ export class SlackAdapter {
68
100
  if (!targetChannel) throw new Error('slack channelId required')
69
101
 
70
102
  const threadTs = opts.threadId || undefined
71
- for (const chunk of chunks) {
103
+ let firstResult = { ok: true }
104
+ for (let i = 0; i < (chunks.length ? chunks.length : 1); i++) {
105
+ const chunk = chunks[i] || ''
106
+ const reqBody = {
107
+ channel: targetChannel,
108
+ text: chunk,
109
+ thread_ts: threadTs,
110
+ }
111
+ if (i === 0 && blocks) reqBody.blocks = blocks
72
112
  const res = await fetch(`${SLACK_API}/chat.postMessage`, {
73
113
  method: 'POST',
74
114
  headers: {
75
115
  Authorization: `Bearer ${this.botToken}`,
76
116
  'Content-Type': 'application/json; charset=utf-8',
77
117
  },
78
- body: JSON.stringify({
79
- channel: targetChannel,
80
- text: chunk,
81
- thread_ts: threadTs,
82
- }),
118
+ body: JSON.stringify(reqBody),
83
119
  })
84
- const json = await res.json().catch(() => ({}))
85
- if (!res.ok || json.ok === false) {
86
- throw new Error(`slack chat.postMessage error: ${json.error || res.status}`)
120
+ const json = typeof res.json === 'function' ? await res.json().catch(() => ({})) : {}
121
+ if (!res.ok || json?.ok === false) {
122
+ throw new Error(`slack chat.postMessage error: ${json?.error || res.status}`)
87
123
  }
124
+ if (i === 0) firstResult = { ok: true, ts: json?.ts }
125
+ }
126
+ return firstResult
127
+ }
128
+
129
+ async editMessage(channelId, ts, text, blocks) {
130
+ if (!this.botToken) throw new Error('slack botToken required to edit message')
131
+ const body = {
132
+ channel: channelId,
133
+ ts: String(ts),
134
+ text: String(text || '').slice(0, SLACK_MAX_LENGTH),
135
+ }
136
+ if (blocks !== undefined) body.blocks = blocks
137
+ const res = await fetch(`${SLACK_API}/chat.update`, {
138
+ method: 'POST',
139
+ headers: {
140
+ Authorization: `Bearer ${this.botToken}`,
141
+ 'Content-Type': 'application/json; charset=utf-8',
142
+ },
143
+ body: JSON.stringify(body),
144
+ })
145
+ const json = typeof res.json === 'function' ? await res.json().catch(() => ({})) : {}
146
+ if (!res.ok || json?.ok === false) {
147
+ throw new Error(`slack chat.update error: ${json?.error || res.status}`)
88
148
  }
89
149
  return { ok: true }
90
150
  }
@@ -243,7 +243,7 @@ export class TelegramAdapter {
243
243
  if (fromId && !this.allowed(fromId)) {
244
244
  await this.call('answerCallbackQuery', {
245
245
  callback_query_id: update.callback_query.id,
246
- text: 'Нет доступа',
246
+ text: 'Access denied',
247
247
  show_alert: true,
248
248
  }).catch(() => {})
249
249
  return
@@ -361,7 +361,7 @@ export class TelegramAdapter {
361
361
  const quoted = replyMsg.text ?? replyMsg.caption ?? ''
362
362
  const quoteFrag = msg.quote?.text || ''
363
363
  replyText = quoteFrag
364
- ? `${quoted}${quoted ? '\n' : ''}[цитата: ${quoteFrag}]`
364
+ ? `${quoted}${quoted ? '\n' : ''}[quote: ${quoteFrag}]`
365
365
  : quoted
366
366
  }
367
367
 
@@ -377,17 +377,17 @@ export class TelegramAdapter {
377
377
  try {
378
378
  const { path } = await this.downloadByFileId(st.file_id, 'sticker-video', '.webm', st.file_unique_id || '')
379
379
  attachments.push({ kind: 'animation', path, mime: 'video/webm', emoji: st.emoji || '', name: `sticker${st.emoji || ''}.webm` })
380
- if (st.emoji) text = `${text}\n[Видео-стикер ${st.emoji}]`.trim()
380
+ if (st.emoji) text = `${text}\n[Video sticker ${st.emoji}]`.trim()
381
381
  } catch (e) {
382
- text = `${text}\n[Видео-стикер ${st.emoji || ''} (не скачан: ${e.message})]`.trim()
382
+ text = `${text}\n[Video sticker ${st.emoji || ''} (download failed: ${e.message})]`.trim()
383
383
  }
384
384
  } else if (st.is_animated) {
385
385
  try {
386
386
  const { path } = await this.downloadByFileId(st.file_id, 'sticker-anim', '.tgs', st.file_unique_id || '')
387
387
  attachments.push({ kind: 'document', path, mime: 'application/x-tgsticker', emoji: st.emoji || '', name: `sticker${st.emoji || ''}.tgs` })
388
- if (st.emoji) text = `${text}\n[Анимированный стикер ${st.emoji}]`.trim()
388
+ if (st.emoji) text = `${text}\n[Animated sticker ${st.emoji}]`.trim()
389
389
  } catch (e) {
390
- text = `${text}\n[Анимированный стикер ${st.emoji || ''} (не скачан: ${e.message})]`.trim()
390
+ text = `${text}\n[Animated sticker ${st.emoji || ''} (download failed: ${e.message})]`.trim()
391
391
  }
392
392
  } else {
393
393
  const { path } = await this.downloadByFileId(st.file_id, 'sticker', '.webp', st.file_unique_id || '')
@@ -467,7 +467,7 @@ export class TelegramAdapter {
467
467
  async startProgressMessage(chatId, replyTo, threadId = 0) {
468
468
  const result = await this.call('sendMessage', {
469
469
  chat_id: chatId,
470
- text: '⏳ Думаю…',
470
+ text: '⏳ Thinking…',
471
471
  reply_to_message_id: replyTo,
472
472
  ...telegramThreadParams(threadId),
473
473
  })
@@ -475,9 +475,9 @@ export class TelegramAdapter {
475
475
  return {
476
476
  messageId,
477
477
  edit: async (text) => {
478
- const plain = String(text || '⏳ Думаю…').slice(0, TELEGRAM_MAX)
478
+ const plain = String(text || '⏳ Thinking…').slice(0, TELEGRAM_MAX)
479
479
  try {
480
- await this.call('editMessageText', { chat_id: chatId, message_id: messageId, text: plain || '⏳ Думаю…' })
480
+ await this.call('editMessageText', { chat_id: chatId, message_id: messageId, text: plain || '⏳ Thinking…' })
481
481
  } catch (err) {
482
482
  const msg = String(err.message || '')
483
483
  if (!msg.includes('message is not modified')) this.logger?.warn?.(`progress edit: ${err.message}`)
package/lib/alerts.js CHANGED
@@ -8,26 +8,26 @@ export function formatAlertMessage(type, payload = {}) {
8
8
  const { userId, username, code } = payload
9
9
  const userStr = username ? `@${username} (id: <code>${userId}</code>)` : `id: <code>${userId}</code>`
10
10
  return [
11
- `🔐 <b>[Запрос сопряжения]</b> <i>(${timestamp})</i>`,
11
+ `🔐 <b>[Pairing Request]</b> <i>(${timestamp})</i>`,
12
12
  '',
13
- `Пользователь: ${userStr}`,
14
- `Код доступа: <code>${code}</code>`,
13
+ `User: ${userStr}`,
14
+ `Pairing Code: <code>${code}</code>`,
15
15
  '',
16
- `Для одобрения отправьте боту:`,
16
+ `To approve, send to bot:`,
17
17
  `<code>/pair ${code}</code>`,
18
18
  ].join('\n')
19
19
  }
20
20
 
21
21
  if (type === 'error') {
22
22
  const { message, code, sessionId, chatId, threadId } = payload
23
- const location = chatId ? `Чат: <code>${chatId}</code>${threadId ? ` / топик <code>${threadId}</code>` : ''}` : ''
24
- const sess = sessionId ? `Сессия: <code>${sessionId}</code>` : ''
23
+ const location = chatId ? `Chat: <code>${chatId}</code>${threadId ? ` / thread <code>${threadId}</code>` : ''}` : ''
24
+ const sess = sessionId ? `Session: <code>${sessionId}</code>` : ''
25
25
  const meta = [location, sess].filter(Boolean).join('\n')
26
26
 
27
27
  return [
28
- `🚨 <b>[Ошибка шлюза]</b> <i>(${timestamp})</i>`,
28
+ `🚨 <b>[Gateway Error]</b> <i>(${timestamp})</i>`,
29
29
  meta ? `\n${meta}` : '',
30
- `Ошибка: <b>${escapeHtml(String(code || 'error'))}</b>`,
30
+ `Error code: <b>${escapeHtml(String(code || 'error'))}</b>`,
31
31
  `<code>${escapeHtml(String(message || 'unknown error'))}</code>`,
32
32
  ].filter(Boolean).join('\n')
33
33
  }
@@ -35,12 +35,12 @@ export function formatAlertMessage(type, payload = {}) {
35
35
  if (type === 'status') {
36
36
  const { title, details } = payload
37
37
  return [
38
- `⚡ <b>[Шлюз: ${escapeHtml(title || 'Статус')}]</b> <i>(${timestamp})</i>`,
38
+ `⚡ <b>[Gateway: ${escapeHtml(title || 'Status')}]</b> <i>(${timestamp})</i>`,
39
39
  details ? `\n${escapeHtml(details)}` : '',
40
40
  ].filter(Boolean).join('\n')
41
41
  }
42
42
 
43
- return `🔔 <b>[Алерт: ${type}]</b> <i>(${timestamp})</i>\n${escapeHtml(JSON.stringify(payload))}`
43
+ return `🔔 <b>[Alert: ${type}]</b> <i>(${timestamp})</i>\n${escapeHtml(JSON.stringify(payload))}`
44
44
  }
45
45
 
46
46
  export function resolveAlertTarget(gateway) {