@goodandready/dsh-messenger-gateway 0.3.22 → 0.4.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 +1 -1
- package/cordis.patch.yml +1 -1
- package/lib/adapters/discord.js +1 -1
- package/lib/adapters/slack.js +1 -1
- package/lib/adapters/telegram-inbound.js +141 -0
- package/lib/adapters/telegram.js +596 -712
- package/lib/alerts.js +104 -63
- package/lib/api-health.js +38 -0
- package/lib/artifacts.js +118 -118
- package/lib/ask.js +203 -152
- package/lib/client.js +1660 -1631
- package/lib/commands.js +126 -74
- package/lib/documents.js +1 -1
- package/lib/file-manager.js +27 -1
- package/lib/forum-mirror.js +105 -0
- package/lib/gateway-callbacks.js +187 -0
- package/lib/gateway-commands.js +584 -0
- package/lib/gateway-turn.js +285 -0
- package/lib/gateway.js +58 -1240
- package/lib/http.js +12 -1
- package/lib/index.js +540 -532
- package/lib/integrations.js +1 -1
- package/lib/media.js +1 -9
- package/lib/models.js +6 -3
- package/lib/outbound.js +1 -1
- package/lib/pairing.js +8 -4
- package/lib/personas.js +8 -4
- package/lib/photos.js +1 -1
- package/lib/scheduler.js +169 -159
- package/lib/stream.js +115 -115
- package/lib/telegram-errors.js +0 -0
- package/lib/topics.js +0 -14
- package/lib/tts.js +0 -10
- package/lib/voice-prefs.js +8 -4
- package/package.json +69 -65
package/lib/integrations.js
CHANGED
package/lib/media.js
CHANGED
|
@@ -40,14 +40,6 @@ export function classifyDocument(ext, mime) {
|
|
|
40
40
|
return 'unsupported'
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
export function mediaKindOf(path) {
|
|
44
|
-
const ext = extname(path).toLowerCase()
|
|
45
|
-
if (IMAGE_EXT_TO_MIME[ext]) return 'photo'
|
|
46
|
-
if (['.ogg', '.opus', '.oga'].includes(ext)) return 'voice'
|
|
47
|
-
if (['.mp3', '.m4a', '.wav', '.flac'].includes(ext)) return 'audio'
|
|
48
|
-
return 'document'
|
|
49
|
-
}
|
|
50
|
-
|
|
51
43
|
export function safeName(name) {
|
|
52
44
|
return String(name || 'file').replace(/[^\w.\- ]/g, '_').slice(0, 120) || 'file'
|
|
53
45
|
}
|
|
@@ -65,4 +57,4 @@ export function cacheName(prefix, ext, name) {
|
|
|
65
57
|
return display ? `${base}-${display}${ext || ''}` : `${base}${ext || ''}`
|
|
66
58
|
}
|
|
67
59
|
|
|
68
|
-
export { basename }
|
|
60
|
+
export { basename }
|
package/lib/models.js
CHANGED
|
@@ -20,11 +20,14 @@ export async function listModelCatalog(ctx, fallback = {}) {
|
|
|
20
20
|
pId,
|
|
21
21
|
(models || []).map((m) => ({ id: m.id || m, name: m.name || m.id || m }))
|
|
22
22
|
)
|
|
23
|
-
} catch {
|
|
23
|
+
} catch (err) {
|
|
24
|
+
/* safe fallback to empty list when provider cannot list models */
|
|
24
25
|
result.modelsByProvider.set(pId, [])
|
|
25
26
|
}
|
|
26
27
|
}
|
|
27
|
-
} catch {
|
|
28
|
+
} catch (err) {
|
|
29
|
+
/* safe fallback when ctx.llm.listProviders is unavailable or throws */
|
|
30
|
+
}
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
// 2. If no providers from ctx.llm, check settings / fallback
|
|
@@ -117,4 +120,4 @@ export function buildModelsKeyboard(providerId, models, currentModel, page = 0,
|
|
|
117
120
|
rows.push([{ text: '🔙 Back to providers', callback_data: 'mdl:back' }])
|
|
118
121
|
|
|
119
122
|
return { inline_keyboard: rows, page: curPage, totalPages }
|
|
120
|
-
}
|
|
123
|
+
}
|
package/lib/outbound.js
CHANGED
package/lib/pairing.js
CHANGED
|
@@ -19,25 +19,29 @@ function writeJsonAtomicSync(filePath, data) {
|
|
|
19
19
|
writeFileSync(tmpPath, JSON.stringify(data, null, 2), 'utf8')
|
|
20
20
|
renameSync(tmpPath, filePath)
|
|
21
21
|
} catch (err) {
|
|
22
|
-
try { unlinkSync(tmpPath) } catch {}
|
|
22
|
+
try { unlinkSync(tmpPath) } catch { /* safe best-effort temp file cleanup */ }
|
|
23
23
|
throw err
|
|
24
24
|
}
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
-
export function createPairingStore(filePath) {
|
|
27
|
+
export function createPairingStore(filePath, { logger } = {}) {
|
|
28
28
|
const state = { pending: {}, /** @type {number[]} */ approved: [] }
|
|
29
29
|
if (filePath && existsSync(filePath)) {
|
|
30
30
|
try {
|
|
31
31
|
const raw = JSON.parse(readFileSync(filePath, 'utf8'))
|
|
32
32
|
if (Array.isArray(raw.approved)) state.approved = raw.approved.map(Number).filter(Number.isFinite)
|
|
33
33
|
if (raw.pending && typeof raw.pending === 'object') state.pending = raw.pending
|
|
34
|
-
} catch {
|
|
34
|
+
} catch (err) {
|
|
35
|
+
logger?.warn?.('[dsh-messenger-gateway] pairing store read error:', err?.message || err)
|
|
36
|
+
}
|
|
35
37
|
}
|
|
36
38
|
const persist = () => {
|
|
37
39
|
if (!filePath) return
|
|
38
40
|
try {
|
|
39
41
|
writeJsonAtomicSync(filePath, { approved: state.approved, pending: state.pending })
|
|
40
|
-
} catch {
|
|
42
|
+
} catch (err) {
|
|
43
|
+
logger?.warn?.('[dsh-messenger-gateway] pairing store persist error:', err?.message || err)
|
|
44
|
+
}
|
|
41
45
|
}
|
|
42
46
|
const prune = () => {
|
|
43
47
|
const now = Date.now()
|
package/lib/personas.js
CHANGED
|
@@ -70,7 +70,7 @@ export function listPersonas() {
|
|
|
70
70
|
return Object.values(BUILTIN_PERSONAS)
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
export function createPersonaStore(filePath) {
|
|
73
|
+
export function createPersonaStore(filePath, { logger } = {}) {
|
|
74
74
|
let cache = { personas: {}, presets: {} }
|
|
75
75
|
try {
|
|
76
76
|
const raw = JSON.parse(readFileSync(filePath, 'utf8'))
|
|
@@ -81,13 +81,17 @@ export function createPersonaStore(filePath) {
|
|
|
81
81
|
cache = { personas: raw, presets: {} }
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
|
-
} catch {
|
|
84
|
+
} catch (err) {
|
|
85
|
+
if (err?.code !== 'ENOENT') logger?.warn?.('[dsh-messenger-gateway] personas load error:', err?.message || err)
|
|
86
|
+
}
|
|
85
87
|
|
|
86
88
|
function save() {
|
|
87
89
|
try {
|
|
88
90
|
mkdirSync(dirname(filePath), { recursive: true })
|
|
89
91
|
writeFileSync(filePath, JSON.stringify(cache, null, 2), 'utf8')
|
|
90
|
-
} catch {
|
|
92
|
+
} catch (err) {
|
|
93
|
+
logger?.warn?.('[dsh-messenger-gateway] personas save error:', err?.message || err)
|
|
94
|
+
}
|
|
91
95
|
}
|
|
92
96
|
|
|
93
97
|
function makeKey(chatId, threadId = 0) {
|
|
@@ -148,4 +152,4 @@ export function createPersonaStore(filePath) {
|
|
|
148
152
|
return { ...cache }
|
|
149
153
|
},
|
|
150
154
|
}
|
|
151
|
-
}
|
|
155
|
+
}
|
package/lib/photos.js
CHANGED
package/lib/scheduler.js
CHANGED
|
@@ -1,160 +1,170 @@
|
|
|
1
|
-
import { readFile } from 'node:fs/promises'
|
|
2
|
-
import { randomUUID } from 'node:crypto'
|
|
3
|
-
import { writeJsonAtomic } from './storage-atomic.js'
|
|
4
|
-
|
|
5
|
-
export function parseRelativeTime(input) {
|
|
6
|
-
if (!input) return null
|
|
7
|
-
const raw = String(input).trim().toLowerCase()
|
|
8
|
-
const match = /^(\d+)\s*(s|sec|m|min|h|hr|d|day|秒|分|小时|天)?$/i.exec(raw)
|
|
9
|
-
if (!match) return null
|
|
10
|
-
|
|
11
|
-
const val = parseInt(match[1], 10)
|
|
12
|
-
if (!Number.isFinite(val) || val <= 0) return null
|
|
13
|
-
|
|
14
|
-
const unit = (match[2] || 'm').toLowerCase()
|
|
15
|
-
if (['s', 'sec', '秒'].includes(unit)) return val * 1000
|
|
16
|
-
if (['m', 'min', '分'].includes(unit)) return val * 60 * 1000
|
|
17
|
-
if (['h', 'hr', '小时'].includes(unit)) return val * 3600 * 1000
|
|
18
|
-
if (['d', 'day', '天'].includes(unit)) return val * 86400 * 1000
|
|
19
|
-
|
|
20
|
-
return null
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function formatRemaining(ms, locale = 'en') {
|
|
24
|
-
const isZh = String(locale || '').toLowerCase().startsWith('zh')
|
|
25
|
-
if (ms <= 0) return isZh ? '刚刚' : 'now'
|
|
26
|
-
const sec = Math.ceil(ms / 1000)
|
|
27
|
-
if (sec < 60) return isZh ? `${sec}秒` : `${sec}s`
|
|
28
|
-
const min = Math.ceil(sec / 60)
|
|
29
|
-
if (min < 60) return isZh ? `${min}分` : `${min}m`
|
|
30
|
-
const hr = Math.floor(min / 60)
|
|
31
|
-
const remMin = min % 60
|
|
32
|
-
if (isZh) return remMin ? `${hr}小时 ${remMin}分` : `${hr}小时`
|
|
33
|
-
return remMin ? `${hr}h ${remMin}m` : `${hr}h`
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function createScheduler(filePath, onDue) {
|
|
37
|
-
|
|
38
|
-
let
|
|
39
|
-
let
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
1
|
+
import { readFile } from 'node:fs/promises'
|
|
2
|
+
import { randomUUID } from 'node:crypto'
|
|
3
|
+
import { writeJsonAtomic } from './storage-atomic.js'
|
|
4
|
+
|
|
5
|
+
export function parseRelativeTime(input) {
|
|
6
|
+
if (!input) return null
|
|
7
|
+
const raw = String(input).trim().toLowerCase()
|
|
8
|
+
const match = /^(\d+)\s*(s|sec|m|min|h|hr|d|day|秒|分|小时|天)?$/i.exec(raw)
|
|
9
|
+
if (!match) return null
|
|
10
|
+
|
|
11
|
+
const val = parseInt(match[1], 10)
|
|
12
|
+
if (!Number.isFinite(val) || val <= 0) return null
|
|
13
|
+
|
|
14
|
+
const unit = (match[2] || 'm').toLowerCase()
|
|
15
|
+
if (['s', 'sec', '秒'].includes(unit)) return val * 1000
|
|
16
|
+
if (['m', 'min', '分'].includes(unit)) return val * 60 * 1000
|
|
17
|
+
if (['h', 'hr', '小时'].includes(unit)) return val * 3600 * 1000
|
|
18
|
+
if (['d', 'day', '天'].includes(unit)) return val * 86400 * 1000
|
|
19
|
+
|
|
20
|
+
return null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function formatRemaining(ms, locale = 'en') {
|
|
24
|
+
const isZh = String(locale || '').toLowerCase().startsWith('zh')
|
|
25
|
+
if (ms <= 0) return isZh ? '刚刚' : 'now'
|
|
26
|
+
const sec = Math.ceil(ms / 1000)
|
|
27
|
+
if (sec < 60) return isZh ? `${sec}秒` : `${sec}s`
|
|
28
|
+
const min = Math.ceil(sec / 60)
|
|
29
|
+
if (min < 60) return isZh ? `${min}分` : `${min}m`
|
|
30
|
+
const hr = Math.floor(min / 60)
|
|
31
|
+
const remMin = min % 60
|
|
32
|
+
if (isZh) return remMin ? `${hr}小时 ${remMin}分` : `${hr}小时`
|
|
33
|
+
return remMin ? `${hr}h ${remMin}m` : `${hr}h`
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function createScheduler(filePath, onDue, options = {}) {
|
|
37
|
+
const logger = options?.logger || console
|
|
38
|
+
let tasks = []
|
|
39
|
+
let loaded = false
|
|
40
|
+
let timer = null
|
|
41
|
+
|
|
42
|
+
async function load() {
|
|
43
|
+
try {
|
|
44
|
+
const raw = await readFile(filePath, 'utf8')
|
|
45
|
+
tasks = JSON.parse(raw)
|
|
46
|
+
} catch (err) {
|
|
47
|
+
if (err?.code !== 'ENOENT') {
|
|
48
|
+
logger?.debug?.('[dsh-messenger-gateway] scheduler load error:', err?.message || err)
|
|
49
|
+
}
|
|
50
|
+
tasks = []
|
|
51
|
+
}
|
|
52
|
+
loaded = true
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const RETENTION_MS = 7 * 86400 * 1000
|
|
56
|
+
|
|
57
|
+
async function save() {
|
|
58
|
+
if (!filePath) return
|
|
59
|
+
try {
|
|
60
|
+
const now = Date.now()
|
|
61
|
+
tasks = tasks.filter((t) => {
|
|
62
|
+
if (t.status === 'pending') return true
|
|
63
|
+
const finishTime = t.firedAt || t.createdAt || 0
|
|
64
|
+
return (now - finishTime) < RETENTION_MS
|
|
65
|
+
})
|
|
66
|
+
await writeJsonAtomic(filePath, tasks)
|
|
67
|
+
} catch (err) {
|
|
68
|
+
logger?.warn?.('[dsh-messenger-gateway] scheduler save failed:', err?.message || err)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async function checkDue() {
|
|
73
|
+
if (!loaded) await load()
|
|
74
|
+
const now = Date.now()
|
|
75
|
+
const due = tasks.filter((t) => t.status === 'pending' && t.dueAt <= now)
|
|
76
|
+
|
|
77
|
+
for (const task of due) {
|
|
78
|
+
if (task.recurring && Number(task.intervalMs) > 0) {
|
|
79
|
+
task.dueAt = now + Number(task.intervalMs)
|
|
80
|
+
task.lastFiredAt = now
|
|
81
|
+
} else {
|
|
82
|
+
task.status = 'fired'
|
|
83
|
+
task.firedAt = now
|
|
84
|
+
}
|
|
85
|
+
if (typeof onDue === 'function') {
|
|
86
|
+
try {
|
|
87
|
+
await onDue(task)
|
|
88
|
+
} catch (err) {
|
|
89
|
+
logger?.warn?.('[dsh-messenger-gateway] scheduler task execution failed:', err?.message || err)
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (due.length > 0) {
|
|
95
|
+
await save()
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function start(intervalMs = 5000) {
|
|
100
|
+
if (timer) clearInterval(timer)
|
|
101
|
+
timer = setInterval(() => {
|
|
102
|
+
checkDue().catch(() => {})
|
|
103
|
+
}, intervalMs)
|
|
104
|
+
timer.unref?.()
|
|
105
|
+
checkDue().catch(() => {})
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function stop() {
|
|
109
|
+
if (timer) {
|
|
110
|
+
clearInterval(timer)
|
|
111
|
+
timer = null
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
async function schedule(taskData) {
|
|
116
|
+
if (!loaded) await load()
|
|
117
|
+
const id = taskData.id || randomUUID().slice(0, 8)
|
|
118
|
+
const task = {
|
|
119
|
+
id,
|
|
120
|
+
platform: taskData.platform || 'telegram',
|
|
121
|
+
chatId: taskData.chatId,
|
|
122
|
+
threadId: taskData.threadId || 0,
|
|
123
|
+
userId: taskData.userId,
|
|
124
|
+
text: taskData.text,
|
|
125
|
+
prompt: taskData.prompt || taskData.text || '',
|
|
126
|
+
dueAt: taskData.dueAt,
|
|
127
|
+
createdAt: Date.now(),
|
|
128
|
+
status: 'pending',
|
|
129
|
+
recurring: Boolean(taskData.recurring),
|
|
130
|
+
intervalMs: taskData.intervalMs || 0,
|
|
131
|
+
cronExpr: taskData.cronExpr || '',
|
|
132
|
+
}
|
|
133
|
+
tasks.push(task)
|
|
134
|
+
await save()
|
|
135
|
+
return task
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function list(chatId) {
|
|
139
|
+
if (!loaded) await load()
|
|
140
|
+
const now = Date.now()
|
|
141
|
+
return tasks.filter((t) => (chatId ? t.chatId === chatId : true) && t.status === 'pending' && t.dueAt > now)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async function listRecurring(chatId) {
|
|
145
|
+
if (!loaded) await load()
|
|
146
|
+
return tasks.filter((t) => (chatId ? t.chatId === chatId : true) && t.status === 'pending' && t.recurring)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function cancel(id, chatId) {
|
|
150
|
+
if (!loaded) await load()
|
|
151
|
+
const task = tasks.find((t) => t.id === id && (chatId ? t.chatId === chatId : true) && t.status === 'pending')
|
|
152
|
+
if (!task) return false
|
|
153
|
+
task.status = 'cancelled'
|
|
154
|
+
await save()
|
|
155
|
+
return true
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
load,
|
|
160
|
+
save,
|
|
161
|
+
start,
|
|
162
|
+
stop,
|
|
163
|
+
schedule,
|
|
164
|
+
list,
|
|
165
|
+
listRecurring,
|
|
166
|
+
cancel,
|
|
167
|
+
checkDue,
|
|
168
|
+
getTasks: () => tasks,
|
|
169
|
+
}
|
|
160
170
|
}
|