@goodandready/dsh-messenger-gateway 0.3.19 → 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
  }