@goodandready/dsh-messenger-gateway 0.3.20 → 0.3.22

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,23 @@ 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),
87
+ keepalive: true,
88
+ signal: AbortSignal.timeout(15000),
60
89
  })
61
90
  if (!res.ok) {
62
91
  const errText = await res.text().catch(() => '')
@@ -77,9 +106,9 @@ export class DiscordAdapter {
77
106
  // Handle files if any on the first chunk
78
107
  if (files.length > 0 && typeof FormData !== 'undefined') {
79
108
  const form = new FormData()
80
- form.append('payload_json', JSON.stringify({
81
- content: chunks[0] || '',
82
- }))
109
+ const payloadJson = { content: chunks[0] || '' }
110
+ if (components) payloadJson.components = components
111
+ form.append('payload_json', JSON.stringify(payloadJson))
83
112
  for (let i = 0; i < files.length; i++) {
84
113
  const file = files[i]
85
114
  const bytes = file.bytes || (file.path ? await readFile(file.path) : null)
@@ -95,37 +124,70 @@ export class DiscordAdapter {
95
124
  Authorization: `Bot ${this.botToken}`,
96
125
  },
97
126
  body: form,
127
+ keepalive: true,
128
+ signal: AbortSignal.timeout(30000),
98
129
  })
99
130
  if (!res.ok) {
100
131
  const errText = await res.text().catch(() => '')
101
132
  throw new Error(`discord send error ${res.status}: ${errText}`)
102
133
  }
134
+ const json = await res.json().catch(() => ({}))
103
135
  // Send remaining text chunks if any
104
136
  for (let i = 1; i < chunks.length; i++) {
105
137
  await this._sendRestMessage(targetChannel, chunks[i])
106
138
  }
107
- return { ok: true }
139
+ return { ok: true, messageId: json.id }
108
140
  }
109
141
 
110
- // Pure text chunks
111
- for (const chunk of chunks) {
112
- await this._sendRestMessage(targetChannel, chunk)
142
+ // Pure text chunks with components on first chunk
143
+ let firstResult = { ok: true }
144
+ for (let i = 0; i < (chunks.length ? chunks.length : 1); i++) {
145
+ const chunk = chunks[i] || ''
146
+ const res = await this._sendRestMessage(targetChannel, chunk, i === 0 ? components : undefined)
147
+ if (i === 0) firstResult = res
148
+ }
149
+ return firstResult
150
+ }
151
+
152
+ async editMessage(channelId, messageId, content, components) {
153
+ if (!this.botToken) throw new Error('discord botToken required to edit message')
154
+ const body = { content: String(content || '').slice(0, DISCORD_MAX_LENGTH) }
155
+ if (components !== undefined) body.components = components
156
+ const res = await fetch(`${DISCORD_API}/channels/${channelId}/messages/${messageId}`, {
157
+ method: 'PATCH',
158
+ headers: {
159
+ Authorization: `Bot ${this.botToken}`,
160
+ 'Content-Type': 'application/json',
161
+ },
162
+ body: JSON.stringify(body),
163
+ keepalive: true,
164
+ signal: AbortSignal.timeout(15000),
165
+ })
166
+ if (!res.ok) {
167
+ const errText = await res.text().catch(() => '')
168
+ throw new Error(`discord edit error ${res.status}: ${errText}`)
113
169
  }
114
170
  return { ok: true }
115
171
  }
116
172
 
117
- async _sendRestMessage(channelId, content) {
173
+ async _sendRestMessage(channelId, content, components) {
174
+ const body = { content }
175
+ if (components && components.length) body.components = components
118
176
  const res = await fetch(`${DISCORD_API}/channels/${channelId}/messages`, {
119
177
  method: 'POST',
120
178
  headers: {
121
179
  Authorization: `Bot ${this.botToken}`,
122
180
  'Content-Type': 'application/json',
123
181
  },
124
- body: JSON.stringify({ content }),
182
+ body: JSON.stringify(body),
183
+ keepalive: true,
184
+ signal: AbortSignal.timeout(15000),
125
185
  })
126
186
  if (!res.ok) {
127
187
  const errText = await res.text().catch(() => '')
128
188
  throw new Error(`discord send error ${res.status}: ${errText}`)
129
189
  }
190
+ const json = typeof res.json === 'function' ? await res.json().catch(() => ({})) : {}
191
+ return { ok: true, messageId: json?.id }
130
192
  }
131
193
  }
@@ -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,21 @@ 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),
86
+ keepalive: true,
87
+ signal: AbortSignal.timeout(15000),
54
88
  })
55
89
  if (!res.ok) {
56
90
  const errText = await res.text().catch(() => '')
@@ -68,23 +102,55 @@ export class SlackAdapter {
68
102
  if (!targetChannel) throw new Error('slack channelId required')
69
103
 
70
104
  const threadTs = opts.threadId || undefined
71
- for (const chunk of chunks) {
105
+ let firstResult = { ok: true }
106
+ for (let i = 0; i < (chunks.length ? chunks.length : 1); i++) {
107
+ const chunk = chunks[i] || ''
108
+ const reqBody = {
109
+ channel: targetChannel,
110
+ text: chunk,
111
+ thread_ts: threadTs,
112
+ }
113
+ if (i === 0 && blocks) reqBody.blocks = blocks
72
114
  const res = await fetch(`${SLACK_API}/chat.postMessage`, {
73
115
  method: 'POST',
74
116
  headers: {
75
117
  Authorization: `Bearer ${this.botToken}`,
76
118
  'Content-Type': 'application/json; charset=utf-8',
77
119
  },
78
- body: JSON.stringify({
79
- channel: targetChannel,
80
- text: chunk,
81
- thread_ts: threadTs,
82
- }),
120
+ body: JSON.stringify(reqBody),
121
+ keepalive: true,
122
+ signal: AbortSignal.timeout(15000),
83
123
  })
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}`)
124
+ const json = typeof res.json === 'function' ? await res.json().catch(() => ({})) : {}
125
+ if (!res.ok || json?.ok === false) {
126
+ throw new Error(`slack chat.postMessage error: ${json?.error || res.status}`)
87
127
  }
128
+ if (i === 0) firstResult = { ok: true, ts: json?.ts }
129
+ }
130
+ return firstResult
131
+ }
132
+
133
+ async editMessage(channelId, ts, text, blocks) {
134
+ if (!this.botToken) throw new Error('slack botToken required to edit message')
135
+ const body = {
136
+ channel: channelId,
137
+ ts: String(ts),
138
+ text: String(text || '').slice(0, SLACK_MAX_LENGTH),
139
+ }
140
+ if (blocks !== undefined) body.blocks = blocks
141
+ const res = await fetch(`${SLACK_API}/chat.update`, {
142
+ method: 'POST',
143
+ headers: {
144
+ Authorization: `Bearer ${this.botToken}`,
145
+ 'Content-Type': 'application/json; charset=utf-8',
146
+ },
147
+ body: JSON.stringify(body),
148
+ keepalive: true,
149
+ signal: AbortSignal.timeout(15000),
150
+ })
151
+ const json = typeof res.json === 'function' ? await res.json().catch(() => ({})) : {}
152
+ if (!res.ok || json?.ok === false) {
153
+ throw new Error(`slack chat.update error: ${json?.error || res.status}`)
88
154
  }
89
155
  return { ok: true }
90
156
  }