@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.
@@ -37,4 +37,4 @@ export async function speakText(baseUrl, text, signal) {
37
37
  mime: data.mime || 'audio/mpeg',
38
38
  provider: data.provider,
39
39
  }
40
- }
40
+ }
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
@@ -110,4 +110,4 @@ export async function buildOutboundFiles(ctx, baseUrl, collector, opts = {}) {
110
110
  }
111
111
  }
112
112
  return files
113
- }
113
+ }
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
@@ -28,4 +28,4 @@ export function photoOnlyHint(attachments, userText) {
28
28
  if (!photos.length) return ''
29
29
  if (photos.length === 1) return '[User attached a photo]'
30
30
  return `[User attached ${photos.length} photos]`
31
- }
31
+ }
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
- let tasks = []
38
- let loaded = false
39
- let timer = null
40
-
41
- async function load() {
42
- try {
43
- const raw = await readFile(filePath, 'utf8')
44
- tasks = JSON.parse(raw)
45
- } catch {
46
- tasks = []
47
- }
48
- loaded = true
49
- }
50
-
51
- const RETENTION_MS = 7 * 86400 * 1000
52
-
53
- async function save() {
54
- if (!filePath) return
55
- try {
56
- const now = Date.now()
57
- tasks = tasks.filter((t) => {
58
- if (t.status === 'pending') return true
59
- const finishTime = t.firedAt || t.createdAt || 0
60
- return (now - finishTime) < RETENTION_MS
61
- })
62
- await writeJsonAtomic(filePath, tasks)
63
- } catch {}
64
- }
65
-
66
- async function checkDue() {
67
- if (!loaded) await load()
68
- const now = Date.now()
69
- const due = tasks.filter((t) => t.status === 'pending' && t.dueAt <= now)
70
-
71
- for (const task of due) {
72
- if (task.recurring && Number(task.intervalMs) > 0) {
73
- task.dueAt = now + Number(task.intervalMs)
74
- task.lastFiredAt = now
75
- } else {
76
- task.status = 'fired'
77
- task.firedAt = now
78
- }
79
- if (typeof onDue === 'function') {
80
- try { await onDue(task) } catch {}
81
- }
82
- }
83
-
84
- if (due.length > 0) {
85
- await save()
86
- }
87
- }
88
-
89
- function start(intervalMs = 5000) {
90
- if (timer) clearInterval(timer)
91
- timer = setInterval(() => {
92
- checkDue().catch(() => {})
93
- }, intervalMs)
94
- timer.unref?.()
95
- checkDue().catch(() => {})
96
- }
97
-
98
- function stop() {
99
- if (timer) {
100
- clearInterval(timer)
101
- timer = null
102
- }
103
- }
104
-
105
- async function schedule(taskData) {
106
- if (!loaded) await load()
107
- const id = taskData.id || randomUUID().slice(0, 8)
108
- const task = {
109
- id,
110
- platform: taskData.platform || 'telegram',
111
- chatId: taskData.chatId,
112
- threadId: taskData.threadId || 0,
113
- userId: taskData.userId,
114
- text: taskData.text,
115
- prompt: taskData.prompt || taskData.text || '',
116
- dueAt: taskData.dueAt,
117
- createdAt: Date.now(),
118
- status: 'pending',
119
- recurring: Boolean(taskData.recurring),
120
- intervalMs: taskData.intervalMs || 0,
121
- cronExpr: taskData.cronExpr || '',
122
- }
123
- tasks.push(task)
124
- await save()
125
- return task
126
- }
127
-
128
- async function list(chatId) {
129
- if (!loaded) await load()
130
- const now = Date.now()
131
- return tasks.filter((t) => (chatId ? t.chatId === chatId : true) && t.status === 'pending' && t.dueAt > now)
132
- }
133
-
134
- async function listRecurring(chatId) {
135
- if (!loaded) await load()
136
- return tasks.filter((t) => (chatId ? t.chatId === chatId : true) && t.status === 'pending' && t.recurring)
137
- }
138
-
139
- async function cancel(id, chatId) {
140
- if (!loaded) await load()
141
- const task = tasks.find((t) => t.id === id && (chatId ? t.chatId === chatId : true) && t.status === 'pending')
142
- if (!task) return false
143
- task.status = 'cancelled'
144
- await save()
145
- return true
146
- }
147
-
148
- return {
149
- load,
150
- save,
151
- start,
152
- stop,
153
- schedule,
154
- list,
155
- listRecurring,
156
- cancel,
157
- checkDue,
158
- getTasks: () => tasks,
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
  }