@goodandready/dsh-messenger-gateway 0.4.5 → 0.4.6
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/lib/gateway-turn.js +288 -288
- package/lib/gateway.js +8 -8
- package/lib/http.js +73 -10
- package/lib/index.js +87 -46
- package/lib/messenger-api.js +1 -0
- package/lib/scheduler.js +169 -169
- package/lib/settings-bridge.js +111 -0
- package/lib/stream.js +8 -5
- package/package.json +3 -1
package/lib/scheduler.js
CHANGED
|
@@ -1,170 +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, 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
|
-
}
|
|
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((err) => { logger?.debug?.('[dsh-messenger-gateway] scheduler checkDue error:', err?.message || err) })
|
|
103
|
+
}, intervalMs)
|
|
104
|
+
timer.unref?.()
|
|
105
|
+
checkDue().catch((err) => { logger?.debug?.('[dsh-messenger-gateway] scheduler checkDue error:', err?.message || err) })
|
|
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
|
+
}
|
|
170
170
|
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
export function setupSettings(ctx, { namespace, Config, resolveConfig, baseConfig, sync, logger }) {
|
|
2
|
+
let settingsApi
|
|
3
|
+
let source = () => resolveConfig(baseConfig ?? {})
|
|
4
|
+
|
|
5
|
+
const mountSettings = (sctx) => {
|
|
6
|
+
const service = sctx?.settings || (typeof sctx?.get === 'function' ? sctx.get('settings') : null)
|
|
7
|
+
const effect = typeof sctx?.effect === 'function' ? sctx.effect.bind(sctx) : (typeof ctx.effect === 'function' ? ctx.effect.bind(ctx) : null)
|
|
8
|
+
if (!service) return
|
|
9
|
+
|
|
10
|
+
if (typeof service.register === 'function') {
|
|
11
|
+
settingsApi = service.register(namespace, Config, { base: baseConfig || {} })
|
|
12
|
+
source = () => resolveConfig(settingsApi.get() ?? baseConfig ?? {})
|
|
13
|
+
if (effect) effect(() => settingsApi.watch(sync), `${namespace}: settings`)
|
|
14
|
+
return
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (typeof service.configure === 'function') {
|
|
18
|
+
try {
|
|
19
|
+
const dispose = service.configure({ auto: false }, ctx.fiber)
|
|
20
|
+
if (effect && typeof dispose === 'function') effect(() => dispose, `${namespace}: configure`)
|
|
21
|
+
} catch (err) {
|
|
22
|
+
logger?.warn?.(`${namespace}: settings configure failed: ${err.message}`)
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const getRevision = () => {
|
|
27
|
+
try {
|
|
28
|
+
return service.describe?.()?.find?.((row) => row.ns === namespace)?.revision
|
|
29
|
+
} catch {
|
|
30
|
+
return undefined
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const readLiveSettings = () => {
|
|
35
|
+
try {
|
|
36
|
+
if (typeof service.describe === 'function') {
|
|
37
|
+
const desc = service.describe()?.find?.((row) => row.ns === namespace)
|
|
38
|
+
if (desc?.value && typeof desc.value === 'object') return desc.value
|
|
39
|
+
}
|
|
40
|
+
if (typeof service.get === 'function') {
|
|
41
|
+
const val = service.get(namespace)
|
|
42
|
+
if (val && typeof val === 'object') return val
|
|
43
|
+
}
|
|
44
|
+
} catch (err) {
|
|
45
|
+
logger?.warn?.(`${namespace}: readLiveSettings failed: ${err.message}`)
|
|
46
|
+
}
|
|
47
|
+
return null
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const liveVal = readLiveSettings()
|
|
51
|
+
let currentConfig = resolveConfig(liveVal ?? baseConfig ?? {})
|
|
52
|
+
source = () => currentConfig
|
|
53
|
+
|
|
54
|
+
const persist = async (next) => {
|
|
55
|
+
const payload = Config(next)
|
|
56
|
+
if (typeof service.replace === 'function') {
|
|
57
|
+
await service.replace(namespace, payload, getRevision())
|
|
58
|
+
} else if (typeof service.update === 'function') {
|
|
59
|
+
await service.update(namespace, payload, getRevision())
|
|
60
|
+
} else if (typeof service.mutate === 'function') {
|
|
61
|
+
await service.mutate(namespace, [{ op: 'set', path: [], value: payload }])
|
|
62
|
+
} else {
|
|
63
|
+
throw new Error('settings service cannot persist configuration')
|
|
64
|
+
}
|
|
65
|
+
currentConfig = resolveConfig(payload)
|
|
66
|
+
sync()
|
|
67
|
+
return currentConfig
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
settingsApi = {
|
|
71
|
+
get: () => source(),
|
|
72
|
+
replace: async (next) => persist(next),
|
|
73
|
+
update: async (patch) => persist({ ...source(), ...patch }),
|
|
74
|
+
watch: (cb) => {
|
|
75
|
+
if (typeof service.watch === 'function') return service.watch(cb)
|
|
76
|
+
return () => {}
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const onFn = typeof sctx?.on === 'function' ? sctx.on.bind(sctx) : (typeof ctx?.on === 'function' ? ctx.on.bind(ctx) : null)
|
|
81
|
+
const disposeDocUpdated = onFn
|
|
82
|
+
? onFn('settings/document-updated', (updatedNs) => {
|
|
83
|
+
if (updatedNs === namespace) {
|
|
84
|
+
const nextVal = readLiveSettings()
|
|
85
|
+
if (nextVal) {
|
|
86
|
+
currentConfig = resolveConfig(nextVal)
|
|
87
|
+
sync()
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
: null
|
|
92
|
+
|
|
93
|
+
if (effect) {
|
|
94
|
+
effect(() => () => {
|
|
95
|
+
if (typeof disposeDocUpdated === 'function') disposeDocUpdated()
|
|
96
|
+
settingsApi = undefined
|
|
97
|
+
}, `${namespace}: settings`)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (typeof ctx.inject === 'function') {
|
|
102
|
+
ctx.inject(['settings'], mountSettings)
|
|
103
|
+
} else {
|
|
104
|
+
mountSettings({ settings: ctx.get?.('settings') || ctx.settings, effect: ctx.effect })
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
getSource: () => source(),
|
|
109
|
+
getSettingsApi: () => settingsApi,
|
|
110
|
+
}
|
|
111
|
+
}
|
package/lib/stream.js
CHANGED
|
@@ -34,7 +34,7 @@ export function parseRetryAfter(err) {
|
|
|
34
34
|
return 0
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
export function createEditScheduler(editFn, intervalMs = 1200, firstChunkDelayMs = 250) {
|
|
37
|
+
export function createEditScheduler(editFn, intervalMs = 1200, firstChunkDelayMs = 250, logger = null) {
|
|
38
38
|
let pending = null
|
|
39
39
|
let timer = null
|
|
40
40
|
let lastSent = ''
|
|
@@ -55,6 +55,7 @@ export function createEditScheduler(editFn, intervalMs = 1200, firstChunkDelayMs
|
|
|
55
55
|
await editFn(text)
|
|
56
56
|
lastSent = text
|
|
57
57
|
} catch (err) {
|
|
58
|
+
logger?.debug?.('stream chunk edit failed:', err?.message || err)
|
|
58
59
|
const retry = parseRetryAfter(err)
|
|
59
60
|
if (retry > 0) {
|
|
60
61
|
retryDelayMs = retry
|
|
@@ -91,20 +92,22 @@ export function createEditScheduler(editFn, intervalMs = 1200, firstChunkDelayMs
|
|
|
91
92
|
try {
|
|
92
93
|
await editFn(pending)
|
|
93
94
|
lastSent = pending
|
|
94
|
-
} catch {
|
|
95
|
+
} catch (err) {
|
|
96
|
+
logger?.debug?.('stream final flush error:', err?.message || err)
|
|
97
|
+
}
|
|
95
98
|
}
|
|
96
99
|
},
|
|
97
100
|
}
|
|
98
101
|
}
|
|
99
102
|
|
|
100
|
-
export function startTypingHeartbeat(typingFn, intervalMs = 4000) {
|
|
103
|
+
export function startTypingHeartbeat(typingFn, intervalMs = 4000, logger = null) {
|
|
101
104
|
if (typeof typingFn !== 'function') return () => {}
|
|
102
105
|
let active = true
|
|
103
106
|
const safeInvoke = () => {
|
|
104
107
|
if (!active) return
|
|
105
108
|
try {
|
|
106
|
-
typingFn()?.catch?.(() => {
|
|
107
|
-
} catch {
|
|
109
|
+
typingFn()?.catch?.((err) => { logger?.debug?.('typing indicator error:', err?.message || err) })
|
|
110
|
+
} catch (err) { logger?.debug?.('typing indicator invoke error:', err?.message || err) }
|
|
108
111
|
}
|
|
109
112
|
safeInvoke()
|
|
110
113
|
const timer = setInterval(safeInvoke, intervalMs)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@goodandready/dsh-messenger-gateway",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.6",
|
|
4
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",
|
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
"lib/",
|
|
16
16
|
"cordis.patch.yml",
|
|
17
17
|
"README.md",
|
|
18
|
+
"README.ru.md",
|
|
19
|
+
"README.zh.md",
|
|
18
20
|
"LICENSE"
|
|
19
21
|
],
|
|
20
22
|
"keywords": [
|