@goodandready/dsh-messenger-gateway 0.1.0
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/LICENSE +21 -0
- package/README.md +35 -0
- package/cordis.patch.yml +4 -0
- package/docs/architecture/2026-08-23-messenger-gateway-design.md +30 -0
- package/docs/deployment/0.1.0-install.md +28 -0
- package/docs/testing/0.1.0-smoke.md +12 -0
- package/lib/adapters/discord.js +6 -0
- package/lib/adapters/index.js +24 -0
- package/lib/adapters/telegram.js +210 -0
- package/lib/ask.js +63 -0
- package/lib/client.js +96 -0
- package/lib/documents.js +16 -0
- package/lib/gateway.js +321 -0
- package/lib/http.js +18 -0
- package/lib/index.js +174 -0
- package/lib/integrations.js +40 -0
- package/lib/media.js +68 -0
- package/lib/messenger-api.js +225 -0
- package/lib/outbound.js +112 -0
- package/lib/photos.js +30 -0
- package/lib/text.js +22 -0
- package/lib/topics.js +27 -0
- package/lib/tts.js +40 -0
- package/package.json +64 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { normalizeThreadId } from './topics.js'
|
|
2
|
+
|
|
3
|
+
const DEFAULT_ASK_TIMEOUT_MS = 300_000
|
|
4
|
+
|
|
5
|
+
export const messengerApiSchema = {
|
|
6
|
+
version: 1,
|
|
7
|
+
service: 'messenger',
|
|
8
|
+
http: {
|
|
9
|
+
send: {
|
|
10
|
+
method: 'POST',
|
|
11
|
+
path: '/dsh-messenger-gateway/messenger/send',
|
|
12
|
+
body: {
|
|
13
|
+
target: { platform: 'telegram', chatId: 'number|string', threadId: 'optional number' },
|
|
14
|
+
text: 'optional string',
|
|
15
|
+
files: 'optional [{ dataBase64, mime, kind, name }]',
|
|
16
|
+
replyMarkup: 'optional Telegram inline_keyboard object',
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
ask: {
|
|
20
|
+
method: 'POST',
|
|
21
|
+
path: '/dsh-messenger-gateway/messenger/ask',
|
|
22
|
+
body: {
|
|
23
|
+
target: { platform: 'telegram', chatId: 'number|string' },
|
|
24
|
+
text: 'string (required)',
|
|
25
|
+
buttons: '[[{ id, text }]] (required, at least one button)',
|
|
26
|
+
timeoutMs: 'optional number (default 300000)',
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
progress: {
|
|
30
|
+
method: 'POST',
|
|
31
|
+
path: '/dsh-messenger-gateway/messenger/progress',
|
|
32
|
+
body: {
|
|
33
|
+
target: { platform: 'telegram', chatId: 'number|string' },
|
|
34
|
+
text: 'string (required)',
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function parseMessengerBody(raw) {
|
|
41
|
+
try {
|
|
42
|
+
return JSON.parse(String(raw || '{}') || '{}')
|
|
43
|
+
} catch {
|
|
44
|
+
const err = new Error('invalid json')
|
|
45
|
+
err.status = 400
|
|
46
|
+
throw err
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function validateTarget(target) {
|
|
51
|
+
if (!target || typeof target !== 'object') {
|
|
52
|
+
const err = new Error('target is required')
|
|
53
|
+
err.status = 400
|
|
54
|
+
throw err
|
|
55
|
+
}
|
|
56
|
+
const platform = String(target.platform || '').trim()
|
|
57
|
+
if (!platform) {
|
|
58
|
+
const err = new Error('target.platform is required')
|
|
59
|
+
err.status = 400
|
|
60
|
+
throw err
|
|
61
|
+
}
|
|
62
|
+
const chatId = target.chatId
|
|
63
|
+
if (chatId === undefined || chatId === null || String(chatId).trim() === '') {
|
|
64
|
+
const err = new Error('target.chatId is required')
|
|
65
|
+
err.status = 400
|
|
66
|
+
throw err
|
|
67
|
+
}
|
|
68
|
+
const out = { platform, chatId }
|
|
69
|
+
const threadId = normalizeThreadId(target.threadId)
|
|
70
|
+
if (threadId > 0) out.threadId = threadId
|
|
71
|
+
return out
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function normalizeButtons(buttons) {
|
|
75
|
+
if (buttons === undefined || buttons === null) return []
|
|
76
|
+
if (!Array.isArray(buttons)) {
|
|
77
|
+
const err = new Error('buttons must be a 2d array')
|
|
78
|
+
err.status = 400
|
|
79
|
+
throw err
|
|
80
|
+
}
|
|
81
|
+
return buttons.map((row, ri) => {
|
|
82
|
+
if (!Array.isArray(row)) {
|
|
83
|
+
const err = new Error(`buttons[${ri}] must be an array`)
|
|
84
|
+
err.status = 400
|
|
85
|
+
throw err
|
|
86
|
+
}
|
|
87
|
+
return row.map((btn, bi) => {
|
|
88
|
+
const id = String(btn?.id ?? btn?.buttonId ?? '').trim()
|
|
89
|
+
const text = String(btn?.text ?? '').trim()
|
|
90
|
+
if (!id || !text) {
|
|
91
|
+
const err = new Error(`buttons[${ri}][${bi}] requires id and text`)
|
|
92
|
+
err.status = 400
|
|
93
|
+
throw err
|
|
94
|
+
}
|
|
95
|
+
return { id, text }
|
|
96
|
+
})
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function normalizeFiles(files) {
|
|
101
|
+
if (!files) return []
|
|
102
|
+
if (!Array.isArray(files)) {
|
|
103
|
+
const err = new Error('files must be an array')
|
|
104
|
+
err.status = 400
|
|
105
|
+
throw err
|
|
106
|
+
}
|
|
107
|
+
return files.map((file, i) => {
|
|
108
|
+
if (!file || typeof file !== 'object') {
|
|
109
|
+
const err = new Error(`files[${i}] must be an object`)
|
|
110
|
+
err.status = 400
|
|
111
|
+
throw err
|
|
112
|
+
}
|
|
113
|
+
if (file.bytes) return file
|
|
114
|
+
if (typeof file.dataBase64 === 'string') {
|
|
115
|
+
return {
|
|
116
|
+
bytes: Buffer.from(file.dataBase64, 'base64'),
|
|
117
|
+
mime: file.mime || 'application/octet-stream',
|
|
118
|
+
kind: file.kind || 'document',
|
|
119
|
+
name: file.name || 'file',
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
const err = new Error(`files[${i}] needs bytes or dataBase64`)
|
|
123
|
+
err.status = 400
|
|
124
|
+
throw err
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function normalizeSendBody(payload) {
|
|
129
|
+
const text = typeof payload?.text === 'string' ? payload.text.trim() : ''
|
|
130
|
+
const files = normalizeFiles(payload?.files)
|
|
131
|
+
if (!text && !files.length) {
|
|
132
|
+
const err = new Error('text or files required')
|
|
133
|
+
err.status = 400
|
|
134
|
+
throw err
|
|
135
|
+
}
|
|
136
|
+
const body = {}
|
|
137
|
+
if (text) body.text = text
|
|
138
|
+
if (files.length) body.files = files
|
|
139
|
+
if (payload?.replyMarkup) body.replyMarkup = payload.replyMarkup
|
|
140
|
+
return body
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function normalizeAskBody(payload) {
|
|
144
|
+
const text = String(payload?.text || '').trim()
|
|
145
|
+
if (!text) {
|
|
146
|
+
const err = new Error('text is required for ask')
|
|
147
|
+
err.status = 400
|
|
148
|
+
throw err
|
|
149
|
+
}
|
|
150
|
+
const buttons = normalizeButtons(payload?.buttons)
|
|
151
|
+
if (!buttons.length || !buttons.some((row) => row.length > 0)) {
|
|
152
|
+
const err = new Error('ask requires at least one button')
|
|
153
|
+
err.status = 400
|
|
154
|
+
throw err
|
|
155
|
+
}
|
|
156
|
+
return { text, buttons }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function normalizeProgressBody(payload) {
|
|
160
|
+
const text = String(payload?.text || '').trim()
|
|
161
|
+
if (!text) {
|
|
162
|
+
const err = new Error('text is required for progress')
|
|
163
|
+
err.status = 400
|
|
164
|
+
throw err
|
|
165
|
+
}
|
|
166
|
+
return { text }
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export function resolveAskTimeoutMs(timeoutMs) {
|
|
170
|
+
const n = Number(timeoutMs)
|
|
171
|
+
if (!Number.isFinite(n) || n <= 0) return DEFAULT_ASK_TIMEOUT_MS
|
|
172
|
+
return Math.min(n, 3_600_000)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export function httpStatusForError(err) {
|
|
176
|
+
if (err?.status) return err.status
|
|
177
|
+
const msg = String(err?.message || '')
|
|
178
|
+
if (msg.includes('timed out')) return 504
|
|
179
|
+
if (msg.includes('not running') || msg.includes('unavailable')) return 503
|
|
180
|
+
return 502
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export async function dispatchMessenger(gw, action, payload) {
|
|
184
|
+
if (!gw) {
|
|
185
|
+
const err = new Error('gateway not running')
|
|
186
|
+
err.status = 503
|
|
187
|
+
throw err
|
|
188
|
+
}
|
|
189
|
+
const target = validateTarget(payload?.target)
|
|
190
|
+
if (action === 'send') {
|
|
191
|
+
await gw.messenger.send(target, normalizeSendBody(payload))
|
|
192
|
+
return { ok: true }
|
|
193
|
+
}
|
|
194
|
+
if (action === 'progress') {
|
|
195
|
+
await gw.messenger.progress(target, normalizeProgressBody(payload))
|
|
196
|
+
return { ok: true }
|
|
197
|
+
}
|
|
198
|
+
if (action === 'ask') {
|
|
199
|
+
const result = await gw.messenger.ask(target, normalizeAskBody(payload), resolveAskTimeoutMs(payload?.timeoutMs))
|
|
200
|
+
return { ok: true, result }
|
|
201
|
+
}
|
|
202
|
+
const err = new Error(`unknown messenger action: ${action}`)
|
|
203
|
+
err.status = 400
|
|
204
|
+
throw err
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function createMessengerService(getGw) {
|
|
208
|
+
return {
|
|
209
|
+
adapters: () => getGw()?.messenger.adapters() ?? [],
|
|
210
|
+
activeChats: () => getGw()?.messenger.activeChats() ?? 0,
|
|
211
|
+
send: async (target, payload) => {
|
|
212
|
+
const gw = getGw()
|
|
213
|
+
await dispatchMessenger(gw, 'send', { target, ...payload })
|
|
214
|
+
},
|
|
215
|
+
progress: async (target, payload) => {
|
|
216
|
+
const gw = getGw()
|
|
217
|
+
await dispatchMessenger(gw, 'progress', { target, ...payload })
|
|
218
|
+
},
|
|
219
|
+
ask: async (target, payload, timeoutMs) => {
|
|
220
|
+
const gw = getGw()
|
|
221
|
+
const out = await dispatchMessenger(gw, 'ask', { target, ...payload, timeoutMs })
|
|
222
|
+
return out.result
|
|
223
|
+
},
|
|
224
|
+
}
|
|
225
|
+
}
|
package/lib/outbound.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
const IMAGE_URL_RE = /(\/(?:dsh-[\w-]+)\/image\?[^\s)]+)/g
|
|
2
|
+
|
|
3
|
+
export function extractImageUrls(text) {
|
|
4
|
+
const urls = []
|
|
5
|
+
for (const m of String(text || '').matchAll(IMAGE_URL_RE)) urls.push(m[1])
|
|
6
|
+
return urls
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Remove harness image URLs from reply text before sending to messenger. */
|
|
10
|
+
export function stripImageUrls(text) {
|
|
11
|
+
return String(text || '')
|
|
12
|
+
.replace(IMAGE_URL_RE, '')
|
|
13
|
+
.replace(/[ \t]+\n/g, '\n')
|
|
14
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
15
|
+
.trim()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Collect image attachment refs from assistant content (incl. nested tool blocks). */
|
|
19
|
+
export function collectImagesDeep(content) {
|
|
20
|
+
const images = []
|
|
21
|
+
const walk = (blocks) => {
|
|
22
|
+
if (!Array.isArray(blocks)) return
|
|
23
|
+
for (const block of blocks) {
|
|
24
|
+
if (!block || typeof block !== 'object') continue
|
|
25
|
+
if (block.type === 'image' && block.attachment) images.push(block.attachment)
|
|
26
|
+
if (Array.isArray(block.content)) walk(block.content)
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
walk(content)
|
|
30
|
+
return images
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function collectAssistantParts(message) {
|
|
34
|
+
const texts = []
|
|
35
|
+
for (const block of message?.content || []) {
|
|
36
|
+
if (block.type === 'text' && block.text) texts.push(block.text)
|
|
37
|
+
}
|
|
38
|
+
return { text: texts.join('\n\n').trim(), images: collectImagesDeep(message?.content) }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function dedupeAttachmentRefs(refs) {
|
|
42
|
+
const seen = new Set()
|
|
43
|
+
const out = []
|
|
44
|
+
for (const ref of refs || []) {
|
|
45
|
+
const id = ref?.attachmentId ?? ref?.id
|
|
46
|
+
const key = id !== undefined ? String(id) : JSON.stringify(ref)
|
|
47
|
+
if (seen.has(key)) continue
|
|
48
|
+
seen.add(key)
|
|
49
|
+
out.push(ref)
|
|
50
|
+
}
|
|
51
|
+
return out
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function fileNameForRef(ref) {
|
|
55
|
+
const name = String(ref?.name || '').trim()
|
|
56
|
+
if (name) return name.replace(/[^\w.\- ]/g, '_').slice(0, 120) || 'image.png'
|
|
57
|
+
const mt = String(ref?.mediaType || '').toLowerCase()
|
|
58
|
+
if (mt === 'image/jpeg' || mt === 'image/jpg') return 'image.jpg'
|
|
59
|
+
if (mt === 'image/webp') return 'image.webp'
|
|
60
|
+
if (mt === 'image/gif') return 'image.gif'
|
|
61
|
+
return 'image.png'
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function readAttachmentBytes(ctx, ref) {
|
|
65
|
+
const stored = await ctx.attachments.readImage(ref)
|
|
66
|
+
return {
|
|
67
|
+
bytes: stored.data,
|
|
68
|
+
mime: ref.mediaType || stored.ref?.mediaType || 'image/png',
|
|
69
|
+
kind: 'photo',
|
|
70
|
+
name: fileNameForRef(ref),
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function fetchInternalImage(baseUrl, path, signal) {
|
|
75
|
+
const url = new URL(path, baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`)
|
|
76
|
+
const res = await fetch(url, { signal })
|
|
77
|
+
if (!res.ok) throw new Error(`image fetch HTTP ${res.status}`)
|
|
78
|
+
const bytes = new Uint8Array(await res.arrayBuffer())
|
|
79
|
+
const mime = res.headers.get('content-type') || 'image/png'
|
|
80
|
+
const fromQuery = url.searchParams.get('id') || url.searchParams.get('name')
|
|
81
|
+
const name = fromQuery ? `image-${fromQuery.slice(0, 32)}.png` : 'image.png'
|
|
82
|
+
return { bytes, mime, kind: 'photo', name }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const DEFAULT_MAX_OUTBOUND_FILES = 10
|
|
86
|
+
|
|
87
|
+
/** Build Telegram-ready files from assistant image blocks and inline plugin URLs. */
|
|
88
|
+
export async function buildOutboundFiles(ctx, baseUrl, collector, opts = {}) {
|
|
89
|
+
const { signal, logger, maxFiles = DEFAULT_MAX_OUTBOUND_FILES } = opts
|
|
90
|
+
const files = []
|
|
91
|
+
const refs = dedupeAttachmentRefs(collector.images)
|
|
92
|
+
for (const ref of refs) {
|
|
93
|
+
if (files.length >= maxFiles) break
|
|
94
|
+
if (signal?.aborted) break
|
|
95
|
+
try {
|
|
96
|
+
files.push(await readAttachmentBytes(ctx, ref))
|
|
97
|
+
} catch (e) {
|
|
98
|
+
logger?.warn?.(`outbound image: ${e.message}`)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const urls = [...new Set(extractImageUrls(collector.parts.join('\n')))]
|
|
102
|
+
for (const path of urls) {
|
|
103
|
+
if (files.length >= maxFiles) break
|
|
104
|
+
if (signal?.aborted) break
|
|
105
|
+
try {
|
|
106
|
+
files.push(await fetchInternalImage(baseUrl, path, signal))
|
|
107
|
+
} catch (e) {
|
|
108
|
+
logger?.warn?.(`outbound url image: ${e.message}`)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return files
|
|
112
|
+
}
|
package/lib/photos.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Persist an inbound photo for the agent. dsh-vision-bridge rewrites image blocks
|
|
5
|
+
* for text-only models at agent/pre-step and llm/stream.
|
|
6
|
+
*/
|
|
7
|
+
export async function attachInboundPhoto(ctx, att, opts = {}) {
|
|
8
|
+
const { signal, maxBytes = 20 * 1024 * 1024 } = opts
|
|
9
|
+
if (signal?.aborted) throw new DOMException('Aborted', 'AbortError')
|
|
10
|
+
const bytes = new Uint8Array(await readFile(att.path))
|
|
11
|
+
if (signal?.aborted) throw new DOMException('Aborted', 'AbortError')
|
|
12
|
+
if (maxBytes > 0 && bytes.length > maxBytes) {
|
|
13
|
+
throw new Error(`image too large (${bytes.length} bytes, max ${maxBytes})`)
|
|
14
|
+
}
|
|
15
|
+
const ref = await ctx.attachments.saveImage({
|
|
16
|
+
data: bytes,
|
|
17
|
+
mediaType: att.mime || 'image/jpeg',
|
|
18
|
+
...(att.name ? { name: att.name } : {}),
|
|
19
|
+
})
|
|
20
|
+
return { ref, byteLength: bytes.length }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Text hint when the user sent photo(s) without a caption. */
|
|
24
|
+
export function photoOnlyHint(attachments, userText) {
|
|
25
|
+
if (String(userText || '').trim()) return ''
|
|
26
|
+
const photos = attachments.filter((a) => a.kind === 'photo')
|
|
27
|
+
if (!photos.length) return ''
|
|
28
|
+
if (photos.length === 1) return '[Пользователь отправил фото]'
|
|
29
|
+
return `[Пользователь отправил ${photos.length} фото]`
|
|
30
|
+
}
|
package/lib/text.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function splitText(text, maxLen) {
|
|
2
|
+
const raw = String(text ?? '')
|
|
3
|
+
if (raw.length <= maxLen) return [raw]
|
|
4
|
+
const chunks = []
|
|
5
|
+
let rest = raw
|
|
6
|
+
while (rest.length > maxLen) {
|
|
7
|
+
let cut = rest.lastIndexOf('\n', maxLen)
|
|
8
|
+
if (cut < maxLen / 2) cut = rest.lastIndexOf(' ', maxLen)
|
|
9
|
+
if (cut < maxLen / 2) cut = maxLen
|
|
10
|
+
chunks.push(rest.slice(0, cut).trim())
|
|
11
|
+
rest = rest.slice(cut).trimStart()
|
|
12
|
+
}
|
|
13
|
+
if (rest.length > 0) chunks.push(rest)
|
|
14
|
+
return chunks
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function assistantText(message) {
|
|
18
|
+
return (message?.content || [])
|
|
19
|
+
.filter((block) => block.type === 'text')
|
|
20
|
+
.map((block) => block.text)
|
|
21
|
+
.join('')
|
|
22
|
+
}
|
package/lib/topics.js
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** Forum topic / thread helpers (Telegram message_thread_id). */
|
|
2
|
+
|
|
3
|
+
export function normalizeThreadId(threadId) {
|
|
4
|
+
const n = Number(threadId)
|
|
5
|
+
if (!Number.isFinite(n) || n <= 0) return 0
|
|
6
|
+
return Math.trunc(n)
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Stable session key: one agent per platform+chat+topic. */
|
|
10
|
+
export function chatKey(platform, chatId, threadId = 0) {
|
|
11
|
+
return `${platform}:${chatId}:${normalizeThreadId(threadId)}`
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function parseChatKey(key) {
|
|
15
|
+
const parts = String(key || '').split(':')
|
|
16
|
+
if (parts.length < 2) return null
|
|
17
|
+
const platform = parts[0]
|
|
18
|
+
const chatId = parts[1]
|
|
19
|
+
const threadId = parts.length >= 3 ? normalizeThreadId(parts[2]) : 0
|
|
20
|
+
return { platform, chatId, threadId }
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Extra Telegram API fields for forum topic replies. */
|
|
24
|
+
export function telegramThreadParams(threadId) {
|
|
25
|
+
const tid = normalizeThreadId(threadId)
|
|
26
|
+
return tid > 0 ? { message_thread_id: tid } : {}
|
|
27
|
+
}
|
package/lib/tts.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/** Prepare agent text for dsh-tts and map audio to Telegram voice messages. */
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_TTS_MAX_CHARS = 4000
|
|
4
|
+
|
|
5
|
+
export function stripMarkdownForSpeech(text) {
|
|
6
|
+
return String(text || '')
|
|
7
|
+
.replace(/```[\s\S]*?```/g, ' ')
|
|
8
|
+
.replace(/`[^`]+`/g, ' ')
|
|
9
|
+
.replace(/!\[[^\]]*\]\([^)]+\)/g, ' ')
|
|
10
|
+
.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1')
|
|
11
|
+
.replace(/https?:\/\/\S+/g, ' ')
|
|
12
|
+
.replace(/[#*_~]/g, '')
|
|
13
|
+
.replace(/\s+/g, ' ')
|
|
14
|
+
.trim()
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function prepareTtsText(answer, maxChars = DEFAULT_TTS_MAX_CHARS) {
|
|
18
|
+
const stripped = stripMarkdownForSpeech(answer)
|
|
19
|
+
if (!stripped || stripped.length < 2) return ''
|
|
20
|
+
const limit = Math.max(1, Number(maxChars) || DEFAULT_TTS_MAX_CHARS)
|
|
21
|
+
return stripped.slice(0, limit)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function voiceFileNameForMime(mime) {
|
|
25
|
+
const m = String(mime || '').toLowerCase()
|
|
26
|
+
if (m.includes('ogg') || m.includes('opus')) return 'reply.ogg'
|
|
27
|
+
if (m.includes('mpeg') || m.includes('mp3')) return 'reply.mp3'
|
|
28
|
+
if (m.includes('wav')) return 'reply.wav'
|
|
29
|
+
return 'reply.dat'
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function voiceReplyFile(spoken) {
|
|
33
|
+
const mime = spoken?.mime || 'audio/mpeg'
|
|
34
|
+
return {
|
|
35
|
+
bytes: spoken.audio,
|
|
36
|
+
mime,
|
|
37
|
+
kind: 'voice',
|
|
38
|
+
name: voiceFileNameForMime(mime),
|
|
39
|
+
}
|
|
40
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@goodandready/dsh-messenger-gateway",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Messenger gateway for DeepSeek Harness: Telegram transport (0.1.0 scope). Discord planned.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./lib/index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./lib/index.js",
|
|
10
|
+
"./client": "./lib/client.js",
|
|
11
|
+
"./package.json": "./package.json",
|
|
12
|
+
"./cordis.patch.yml": "./cordis.patch.yml"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"lib/",
|
|
16
|
+
"cordis.patch.yml",
|
|
17
|
+
"README.md",
|
|
18
|
+
"LICENSE",
|
|
19
|
+
"docs/"
|
|
20
|
+
],
|
|
21
|
+
"keywords": [
|
|
22
|
+
"dsh",
|
|
23
|
+
"dsh-plugin",
|
|
24
|
+
"telegram",
|
|
25
|
+
"discord",
|
|
26
|
+
"messenger",
|
|
27
|
+
"deepseek-harness"
|
|
28
|
+
],
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/GooDAnDReaDY/dsh-messenger-gateway.git"
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://github.com/GooDAnDReaDY/dsh-messenger-gateway",
|
|
34
|
+
"scripts": {
|
|
35
|
+
"test": "node --test test/*.test.mjs"
|
|
36
|
+
},
|
|
37
|
+
"dsh": {
|
|
38
|
+
"bundle": {
|
|
39
|
+
"patch": "./cordis.patch.yml"
|
|
40
|
+
},
|
|
41
|
+
"client": {
|
|
42
|
+
"platform": "web",
|
|
43
|
+
"inject": [
|
|
44
|
+
"@deepseek-ai/dsh-client-runtime",
|
|
45
|
+
"@deepseek-ai/dsh-client-locale",
|
|
46
|
+
"@deepseek-ai/dsh-client-ui-slots",
|
|
47
|
+
"@deepseek-ai/dsh-client-ui-settings"
|
|
48
|
+
]
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
"peerDependencies": {
|
|
52
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
53
|
+
"@deepseek-ai/dsh-agent": "^0.1.0-rc.6",
|
|
54
|
+
"@deepseek-ai/dsh-agent-loop": "^0.1.0-rc.6",
|
|
55
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6",
|
|
56
|
+
"@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
|
|
57
|
+
"@deepseek-ai/dsh-session": "^0.1.0-rc.6",
|
|
58
|
+
"@deepseek-ai/dsh-settings": "^0.1.0-rc.6",
|
|
59
|
+
"@deepseek-ai/schemastery": "^3.18.1"
|
|
60
|
+
},
|
|
61
|
+
"publishConfig": {
|
|
62
|
+
"access": "public"
|
|
63
|
+
}
|
|
64
|
+
}
|