@goodandready/dsh-messenger-gateway 0.1.0 → 0.2.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/README.md +68 -19
- package/lib/adapters/index.js +12 -2
- package/lib/adapters/telegram.js +324 -47
- package/lib/client.js +471 -46
- package/lib/commands.js +30 -0
- package/lib/config.js +75 -0
- package/lib/documents.js +19 -5
- package/lib/gateway.js +476 -55
- package/lib/groups.js +72 -0
- package/lib/homes.js +58 -0
- package/lib/index.js +261 -39
- package/lib/messenger-api.js +5 -5
- package/lib/pairing.js +110 -0
- package/lib/stream.js +64 -0
- package/lib/telegram-format.js +39 -0
- package/lib/text.js +18 -1
- package/lib/topics.js +23 -4
- package/lib/tts.js +82 -2
- package/lib/voice-prefs.js +44 -0
- package/package.json +15 -9
- package/docs/architecture/2026-08-23-messenger-gateway-design.md +0 -30
- package/docs/deployment/0.1.0-install.md +0 -28
- package/docs/testing/0.1.0-smoke.md +0 -12
package/lib/stream.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/** Helpers for Telegram streaming edits + progress. */
|
|
2
|
+
|
|
3
|
+
export function extractTextDelta(chunk) {
|
|
4
|
+
if (!chunk || typeof chunk !== 'object') return ''
|
|
5
|
+
if (chunk.type === 'text-delta' && typeof chunk.text === 'string') return chunk.text
|
|
6
|
+
return ''
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function extractToolName(eventData) {
|
|
10
|
+
const name = eventData?.tool?.name || eventData?.toolName || eventData?.name
|
|
11
|
+
return name ? String(name) : ''
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function formatProgressLine(toolName) {
|
|
15
|
+
if (!toolName) return '⏳ Думаю…'
|
|
16
|
+
return `🔧 ${toolName}…`
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function buildStreamPreview(streamText, toolName, maxLen = 3500) {
|
|
20
|
+
const body = String(streamText || '').trimEnd()
|
|
21
|
+
const head = toolName ? `${formatProgressLine(toolName)}\n\n` : ''
|
|
22
|
+
const combined = head + (body || '…')
|
|
23
|
+
if (combined.length <= maxLen) return combined
|
|
24
|
+
return combined.slice(0, maxLen - 1) + '…'
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function createEditScheduler(editFn, intervalMs = 1200) {
|
|
28
|
+
let pending = null
|
|
29
|
+
let timer = null
|
|
30
|
+
let lastSent = ''
|
|
31
|
+
let inFlight = Promise.resolve()
|
|
32
|
+
const flush = () => {
|
|
33
|
+
timer = null
|
|
34
|
+
if (pending === null || pending === lastSent) return
|
|
35
|
+
const text = pending
|
|
36
|
+
inFlight = inFlight.then(async () => {
|
|
37
|
+
try {
|
|
38
|
+
await editFn(text)
|
|
39
|
+
lastSent = text
|
|
40
|
+
} catch {}
|
|
41
|
+
})
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
push(text) {
|
|
45
|
+
pending = text
|
|
46
|
+
if (timer) return
|
|
47
|
+
timer = setTimeout(flush, Math.max(200, intervalMs))
|
|
48
|
+
timer.unref?.()
|
|
49
|
+
},
|
|
50
|
+
async flush() {
|
|
51
|
+
if (timer) { clearTimeout(timer); timer = null }
|
|
52
|
+
flush()
|
|
53
|
+
await inFlight
|
|
54
|
+
},
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function startTypingHeartbeat(typingFn, intervalMs = 4000) {
|
|
59
|
+
if (typeof typingFn !== 'function') return () => {}
|
|
60
|
+
typingFn().catch?.(() => {})
|
|
61
|
+
const timer = setInterval(() => { typingFn().catch?.(() => {}) }, intervalMs)
|
|
62
|
+
timer.unref?.()
|
|
63
|
+
return () => clearInterval(timer)
|
|
64
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export function escapeHtml(text) {
|
|
2
|
+
return String(text ?? '')
|
|
3
|
+
.replace(/&/g, '&')
|
|
4
|
+
.replace(/</g, '<')
|
|
5
|
+
.replace(/>/g, '>')
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const PH = '\uE000'
|
|
9
|
+
|
|
10
|
+
/** Convert common Markdown from LLM replies to Telegram HTML parse_mode. */
|
|
11
|
+
export function markdownToTelegramHtml(markdown) {
|
|
12
|
+
let text = String(markdown ?? '')
|
|
13
|
+
const slots = []
|
|
14
|
+
|
|
15
|
+
const stash = (html) => {
|
|
16
|
+
const id = slots.length
|
|
17
|
+
slots.push(html)
|
|
18
|
+
return `${PH}${id}${PH}`
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
text = text.replace(/```([\s\S]*?)```/g, (_, code) => stash(`<pre><code>${escapeHtml(code.replace(/^\n/, '').replace(/\n$/, ''))}</code></pre>`))
|
|
22
|
+
text = text.replace(/`([^`\n]+)`/g, (_, code) => stash(`<code>${escapeHtml(code)}</code>`))
|
|
23
|
+
text = escapeHtml(text)
|
|
24
|
+
text = text.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_, label, url) => `<a href="${escapeHtml(url)}">${label}</a>`)
|
|
25
|
+
text = text.replace(/\*\*([^*\n]+)\*\*/g, '<b>$1</b>')
|
|
26
|
+
text = text.replace(/__([^_\n]+)__/g, '<b>$1</b>')
|
|
27
|
+
text = text.replace(/~~([^~\n]+)~~/g, '<s>$1</s>')
|
|
28
|
+
text = text.replace(/(^|[^*])\*([^*\n]+)\*(?!\*)/g, '$1<i>$2</i>')
|
|
29
|
+
text = text.replace(/(^|[^_])_([^_\n]+)_(?!_)/g, '$1<i>$2</i>')
|
|
30
|
+
text = text.replace(/^#{1,6}\s+(.+)$/gm, '<b>$1</b>')
|
|
31
|
+
for (let i = 0; i < slots.length; i++) text = text.replace(`${PH}${i}${PH}`, slots[i])
|
|
32
|
+
return text
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function prepareTelegramText(text, format = 'html') {
|
|
36
|
+
const raw = String(text ?? '')
|
|
37
|
+
if (!raw || format === 'plain') return { text: raw, parseMode: undefined }
|
|
38
|
+
return { text: markdownToTelegramHtml(raw), parseMode: 'HTML' }
|
|
39
|
+
}
|
package/lib/text.js
CHANGED
|
@@ -19,4 +19,21 @@ export function assistantText(message) {
|
|
|
19
19
|
.filter((block) => block.type === 'text')
|
|
20
20
|
.map((block) => block.text)
|
|
21
21
|
.join('')
|
|
22
|
-
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
/** Drop English reasoning preamble before the user-facing reply (Telegram). */
|
|
26
|
+
export function stripReasoningPreamble(text) {
|
|
27
|
+
const raw = String(text ?? '').trim()
|
|
28
|
+
if (!raw) return raw
|
|
29
|
+
const paragraphs = raw.split(/\n\n+/)
|
|
30
|
+
for (let i = 0; i < paragraphs.length; i++) {
|
|
31
|
+
const p = paragraphs[i]
|
|
32
|
+
const cyrillic = (p.match(/[\u0400-\u04FF]/g) || []).length
|
|
33
|
+
const latin = (p.match(/[A-Za-z]/g) || []).length
|
|
34
|
+
if (cyrillic >= 12 && cyrillic > latin) return paragraphs.slice(i).join('\n\n').trim()
|
|
35
|
+
}
|
|
36
|
+
return raw
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const MESSENGER_RELAY_INSTRUCTION = 'Канал: Telegram. Отвечай только пользователю по-русски. Не выводи ход рассуждений, планирование и служебный текст на английском.'
|
package/lib/topics.js
CHANGED
|
@@ -6,22 +6,41 @@ export function normalizeThreadId(threadId) {
|
|
|
6
6
|
return Math.trunc(n)
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
/** Stable
|
|
9
|
+
/** Stable base key: platform+chat+topic. */
|
|
10
10
|
export function chatKey(platform, chatId, threadId = 0) {
|
|
11
11
|
return `${platform}:${chatId}:${normalizeThreadId(threadId)}`
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Session map key.
|
|
16
|
+
* scope=user in groups → one agent per user in that chat/topic.
|
|
17
|
+
* Private chats ignore user suffix (already 1:1).
|
|
18
|
+
*/
|
|
19
|
+
export function sessionKey({ platform, chatId, threadId = 0, userId, chatType, scope = "chat" }) {
|
|
20
|
+
const base = chatKey(platform, chatId, threadId)
|
|
21
|
+
const isGroup = chatType === "group" || chatType === "supergroup"
|
|
22
|
+
if (scope === "user" && isGroup && userId != null && Number.isFinite(Number(userId))) {
|
|
23
|
+
return `${base}:u:${Number(userId)}`
|
|
24
|
+
}
|
|
25
|
+
return base
|
|
26
|
+
}
|
|
27
|
+
|
|
14
28
|
export function parseChatKey(key) {
|
|
15
|
-
const parts = String(key ||
|
|
29
|
+
const parts = String(key || "").split(":")
|
|
16
30
|
if (parts.length < 2) return null
|
|
17
31
|
const platform = parts[0]
|
|
18
32
|
const chatId = parts[1]
|
|
19
33
|
const threadId = parts.length >= 3 ? normalizeThreadId(parts[2]) : 0
|
|
20
|
-
|
|
34
|
+
const out = { platform, chatId, threadId }
|
|
35
|
+
if (parts.length >= 5 && parts[3] === "u") {
|
|
36
|
+
const userId = Number(parts[4])
|
|
37
|
+
if (Number.isFinite(userId)) out.userId = userId
|
|
38
|
+
}
|
|
39
|
+
return out
|
|
21
40
|
}
|
|
22
41
|
|
|
23
42
|
/** Extra Telegram API fields for forum topic replies. */
|
|
24
43
|
export function telegramThreadParams(threadId) {
|
|
25
44
|
const tid = normalizeThreadId(threadId)
|
|
26
45
|
return tid > 0 ? { message_thread_id: tid } : {}
|
|
27
|
-
}
|
|
46
|
+
}
|
package/lib/tts.js
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
/** Prepare agent text for dsh-tts and map audio to Telegram voice
|
|
1
|
+
/** Prepare agent text for dsh-tts and map audio to Telegram voice notes. */
|
|
2
|
+
|
|
3
|
+
import { spawn } from 'node:child_process'
|
|
4
|
+
import { mkdtemp, writeFile, readFile, rm } from 'node:fs/promises'
|
|
5
|
+
import { tmpdir } from 'node:os'
|
|
6
|
+
import { join } from 'node:path'
|
|
2
7
|
|
|
3
8
|
export const DEFAULT_TTS_MAX_CHARS = 4000
|
|
4
9
|
|
|
@@ -29,6 +34,11 @@ export function voiceFileNameForMime(mime) {
|
|
|
29
34
|
return 'reply.dat'
|
|
30
35
|
}
|
|
31
36
|
|
|
37
|
+
export function isOggOpusMime(mime) {
|
|
38
|
+
const m = String(mime || '').toLowerCase()
|
|
39
|
+
return m.includes('ogg') || m.includes('opus')
|
|
40
|
+
}
|
|
41
|
+
|
|
32
42
|
export function voiceReplyFile(spoken) {
|
|
33
43
|
const mime = spoken?.mime || 'audio/mpeg'
|
|
34
44
|
return {
|
|
@@ -37,4 +47,74 @@ export function voiceReplyFile(spoken) {
|
|
|
37
47
|
kind: 'voice',
|
|
38
48
|
name: voiceFileNameForMime(mime),
|
|
39
49
|
}
|
|
40
|
-
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function runFfmpeg(args) {
|
|
53
|
+
return new Promise((resolve, reject) => {
|
|
54
|
+
const child = spawn('ffmpeg', args, { stdio: ['ignore', 'ignore', 'pipe'] })
|
|
55
|
+
let err = ''
|
|
56
|
+
child.stderr.on('data', (chunk) => { err += String(chunk) })
|
|
57
|
+
child.on('error', reject)
|
|
58
|
+
child.on('close', (code) => {
|
|
59
|
+
if (code === 0) resolve()
|
|
60
|
+
else reject(new Error(`ffmpeg exit ${code}: ${err.slice(-400)}`))
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Convert arbitrary TTS audio bytes to OGG/Opus for Telegram sendVoice. */
|
|
66
|
+
export async function convertToOggOpus(audioBytes, inputMime = 'audio/mpeg') {
|
|
67
|
+
const dir = await mkdtemp(join(tmpdir(), 'msgw-tts-'))
|
|
68
|
+
const ext = voiceFileNameForMime(inputMime).split('.').pop() || 'mp3'
|
|
69
|
+
const input = join(dir, `in.${ext}`)
|
|
70
|
+
const output = join(dir, 'out.ogg')
|
|
71
|
+
try {
|
|
72
|
+
await writeFile(input, audioBytes)
|
|
73
|
+
await runFfmpeg([
|
|
74
|
+
'-y', '-hide_banner', '-loglevel', 'error',
|
|
75
|
+
'-i', input,
|
|
76
|
+
'-c:a', 'libopus',
|
|
77
|
+
'-b:a', '32k',
|
|
78
|
+
'-vbr', 'on',
|
|
79
|
+
'-application', 'voip',
|
|
80
|
+
output,
|
|
81
|
+
])
|
|
82
|
+
return await readFile(output)
|
|
83
|
+
} finally {
|
|
84
|
+
await rm(dir, { recursive: true, force: true }).catch(() => {})
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Build a Telegram voice-note file from dsh-tts output.
|
|
90
|
+
* Prefer OGG/Opus; if conversion fails and binary missing, fall back to audio (not voice-note).
|
|
91
|
+
*/
|
|
92
|
+
export async function toTelegramVoiceFile(spoken, { logger } = {}) {
|
|
93
|
+
if (!spoken?.audio?.length) return null
|
|
94
|
+
if (isOggOpusMime(spoken.mime)) {
|
|
95
|
+
return {
|
|
96
|
+
bytes: spoken.audio,
|
|
97
|
+
mime: 'audio/ogg',
|
|
98
|
+
kind: 'voice',
|
|
99
|
+
name: 'reply.ogg',
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
const ogg = await convertToOggOpus(spoken.audio, spoken.mime)
|
|
104
|
+
return {
|
|
105
|
+
bytes: ogg,
|
|
106
|
+
mime: 'audio/ogg',
|
|
107
|
+
kind: 'voice',
|
|
108
|
+
name: 'reply.ogg',
|
|
109
|
+
}
|
|
110
|
+
} catch (err) {
|
|
111
|
+
logger?.warn?.(`tts opus convert skipped: ${err.message}`)
|
|
112
|
+
// Telegram sendVoice rejects mp3; send as audio file instead of failing silently.
|
|
113
|
+
return {
|
|
114
|
+
bytes: spoken.audio,
|
|
115
|
+
mime: spoken.mime || 'audio/mpeg',
|
|
116
|
+
kind: 'audio',
|
|
117
|
+
name: voiceFileNameForMime(spoken.mime),
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs"
|
|
2
|
+
import { dirname } from "node:path"
|
|
3
|
+
|
|
4
|
+
export function createVoicePrefs(filePath) {
|
|
5
|
+
/** @type {Record<string, boolean>} */
|
|
6
|
+
let state = {}
|
|
7
|
+
if (filePath && existsSync(filePath)) {
|
|
8
|
+
try {
|
|
9
|
+
const raw = JSON.parse(readFileSync(filePath, "utf8"))
|
|
10
|
+
if (raw && typeof raw === "object") state = raw
|
|
11
|
+
} catch {}
|
|
12
|
+
}
|
|
13
|
+
const persist = () => {
|
|
14
|
+
if (!filePath) return
|
|
15
|
+
mkdirSync(dirname(filePath), { recursive: true })
|
|
16
|
+
writeFileSync(filePath, JSON.stringify(state, null, 2))
|
|
17
|
+
}
|
|
18
|
+
const key = (userId) => String(Number(userId) || userId || "")
|
|
19
|
+
return {
|
|
20
|
+
get(userId) {
|
|
21
|
+
const k = key(userId)
|
|
22
|
+
if (!k || !(k in state)) return null
|
|
23
|
+
return Boolean(state[k])
|
|
24
|
+
},
|
|
25
|
+
set(userId, enabled) {
|
|
26
|
+
const k = key(userId)
|
|
27
|
+
if (!k) return false
|
|
28
|
+
state[k] = Boolean(enabled)
|
|
29
|
+
persist()
|
|
30
|
+
return true
|
|
31
|
+
},
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Decide whether to speak the reply. */
|
|
36
|
+
export function shouldSpeakReply({ globalTts, voiceMode, inboundWasVoice, userPref }) {
|
|
37
|
+
if (globalTts) return true
|
|
38
|
+
if (userPref === true) return true
|
|
39
|
+
if (userPref === false) return false
|
|
40
|
+
if (voiceMode === "always") return true
|
|
41
|
+
if (voiceMode === "off") return false
|
|
42
|
+
// mirror: speak if inbound was voice
|
|
43
|
+
return Boolean(inboundWasVoice)
|
|
44
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-messenger-gateway",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.2.0",
|
|
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",
|
|
7
7
|
"main": "./lib/index.js",
|
|
@@ -15,24 +15,26 @@
|
|
|
15
15
|
"lib/",
|
|
16
16
|
"cordis.patch.yml",
|
|
17
17
|
"README.md",
|
|
18
|
-
"LICENSE"
|
|
19
|
-
"docs/"
|
|
18
|
+
"LICENSE"
|
|
20
19
|
],
|
|
21
20
|
"keywords": [
|
|
22
21
|
"dsh",
|
|
23
22
|
"dsh-plugin",
|
|
24
23
|
"telegram",
|
|
25
|
-
"discord",
|
|
26
24
|
"messenger",
|
|
27
|
-
"deepseek-harness"
|
|
25
|
+
"deepseek-harness",
|
|
26
|
+
"bot"
|
|
28
27
|
],
|
|
29
28
|
"repository": {
|
|
30
29
|
"type": "git",
|
|
31
30
|
"url": "git+https://github.com/GooDAnDReaDY/dsh-messenger-gateway.git"
|
|
32
31
|
},
|
|
33
|
-
"homepage": "https://github.com/GooDAnDReaDY/dsh-messenger-gateway",
|
|
32
|
+
"homepage": "https://github.com/GooDAnDReaDY/dsh-messenger-gateway#readme",
|
|
34
33
|
"scripts": {
|
|
35
|
-
"test": "node --test test/*.test.mjs"
|
|
34
|
+
"test": "node --test test/*.test.mjs",
|
|
35
|
+
"test:load": "node scripts/verify-dsh-load.mjs",
|
|
36
|
+
"test:all": "npm test && npm run test:load",
|
|
37
|
+
"smoke:http": "node scripts/smoke-http.mjs"
|
|
36
38
|
},
|
|
37
39
|
"dsh": {
|
|
38
40
|
"bundle": {
|
|
@@ -56,9 +58,13 @@
|
|
|
56
58
|
"@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
|
|
57
59
|
"@deepseek-ai/dsh-session": "^0.1.0-rc.6",
|
|
58
60
|
"@deepseek-ai/dsh-settings": "^0.1.0-rc.6",
|
|
59
|
-
"@deepseek-ai/schemastery": "^3.18.1"
|
|
61
|
+
"@deepseek-ai/schemastery": "^3.18.1",
|
|
62
|
+
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6"
|
|
60
63
|
},
|
|
61
64
|
"publishConfig": {
|
|
62
65
|
"access": "public"
|
|
66
|
+
},
|
|
67
|
+
"bugs": {
|
|
68
|
+
"url": "https://github.com/GooDAnDReaDY/dsh-messenger-gateway/issues"
|
|
63
69
|
}
|
|
64
70
|
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
# dsh-messenger-gateway — versioning
|
|
2
|
-
|
|
3
|
-
## Правило версий
|
|
4
|
-
|
|
5
|
-
| Версия | Когда |
|
|
6
|
-
|--------|--------|
|
|
7
|
-
| **0.0.1, 0.0.2, 0.0.3…** | Каждая отдельная фича (один bump = одна фича) |
|
|
8
|
-
| **0.1.0** | Финальный релиз согласованного scope (замена hub на prod) |
|
|
9
|
-
| **1.0.0** | Когда-нибудь потом, не планируем сейчас |
|
|
10
|
-
|
|
11
|
-
## Журнал 0.0.x
|
|
12
|
-
|
|
13
|
-
| Ver | Фича | Статус |
|
|
14
|
-
|-----|------|--------|
|
|
15
|
-
| 0.0.1 | Скелет: Telegram text, Settings, команды | done |
|
|
16
|
-
| 0.0.2 | Черновик: media, outbound, messenger HTTP, topics, TTS toggle, /stop | done (unit tests) |
|
|
17
|
-
| 0.0.3 | Voice inbound via dsh-voice (signal, errors, unit test) | done (E2E smoke — с ботом) |
|
|
18
|
-
| 0.0.4 | Photo inbound + vision-bridge (attachInboundPhoto, maxImageBytes, photoOnlyHint) | done |
|
|
19
|
-
| 0.0.5 | Documents inbound (formatInboundDocument, documentOnlyHint) | done |
|
|
20
|
-
| 0.0.6 | Outbound tool attachments (nested images, URL fetch, stripImageUrls) | done |
|
|
21
|
-
| 0.0.7 | messenger API (HTTP + ctx.provide messenger, validation, schema) | done |
|
|
22
|
-
| 0.0.8 | Inline keyboards ask (lib/ask.js, cleanup, chat guard) | done |
|
|
23
|
-
| 0.0.9 | Forum topics (threadId sessions + outbound message_thread_id) | done |
|
|
24
|
-
| 0.0.10 | TTS replies (prepareTtsText, AbortSignal, maxChars UI) | done |
|
|
25
|
-
| 0.1.0 | Final Telegram scope + prod swap vs hub-media | done |
|
|
26
|
-
| … | Discord | after 0.1.0 |
|
|
27
|
-
|
|
28
|
-
## Scope 0.1.0 (финал этой линии)
|
|
29
|
-
|
|
30
|
-
Всё из roadmap п.4 для Telegram, без Discord (Discord — после 0.1.0 или отдельной веткой 0.0.x).
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
# 0.1.0 install and prod swap
|
|
2
|
-
|
|
3
|
-
## Prerequisites
|
|
4
|
-
|
|
5
|
-
Profile bundles must include:
|
|
6
|
-
|
|
7
|
-
- `dsh-voice`
|
|
8
|
-
- `dsh-tts` (optional)
|
|
9
|
-
- `dsh-vision-bridge`
|
|
10
|
-
|
|
11
|
-
## Staging smoke
|
|
12
|
-
|
|
13
|
-
1. Backup profile `package.json`.
|
|
14
|
-
2. `dsh plugin --profile <staging-profile> add file:/opt/plugins-dev/dsh-messenger-gateway`
|
|
15
|
-
or `add @goodandready/dsh-messenger-gateway@0.1.0` after npm publish.
|
|
16
|
-
3. Restart Harness; verify client.js 200 and `/dsh-messenger-gateway/status`.
|
|
17
|
-
4. Run `docs/testing/0.1.0-smoke.md`.
|
|
18
|
-
|
|
19
|
-
## RELEASE → npm
|
|
20
|
-
|
|
21
|
-
Copy to RELEASE/dhsplugins, `npm test`, `npm publish`.
|
|
22
|
-
|
|
23
|
-
## Prod swap (user approval only)
|
|
24
|
-
|
|
25
|
-
```bash
|
|
26
|
-
dsh plugin --profile web remove dsh-im-hub-media
|
|
27
|
-
dsh plugin --profile web add @goodandready/dsh-messenger-gateway@0.1.0
|
|
28
|
-
```
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
# 0.1.0 manual smoke
|
|
2
|
-
|
|
3
|
-
- [ ] `/start`, text reply
|
|
4
|
-
- [ ] Voice → dsh-voice transcript
|
|
5
|
-
- [ ] Photo → vision-bridge
|
|
6
|
-
- [ ] Document metadata in prompt
|
|
7
|
-
- [ ] Outbound tool image
|
|
8
|
-
- [ ] messenger HTTP send/ask/progress
|
|
9
|
-
- [ ] Inline keyboard ask
|
|
10
|
-
- [ ] Forum topic session
|
|
11
|
-
- [ ] TTS voice reply (or log warn if down)
|
|
12
|
-
- [ ] `/new` `/stop` `/whoami`
|