@goodandready/dsh-messenger-gateway 0.4.10 → 0.4.11
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/ask.js +3 -1
- package/lib/documents.js +33 -7
- package/lib/messenger-api.js +23 -1
- package/lib/outbound.js +51 -6
- package/lib/personas.js +3 -4
- package/lib/scheduler.js +12 -4
- package/lib/settings-bridge.js +2 -2
- package/lib/storage-atomic.js +17 -0
- package/lib/updater.js +11 -6
- package/package.json +1 -1
package/lib/ask.js
CHANGED
|
@@ -56,7 +56,9 @@ export function buildInlineKeyboard(token, buttons) {
|
|
|
56
56
|
|
|
57
57
|
export function normalizeAskOptions(options = []) {
|
|
58
58
|
if (Array.isArray(options)) {
|
|
59
|
-
const flat =
|
|
59
|
+
const flat = typeof options.flat === 'function'
|
|
60
|
+
? options.flat()
|
|
61
|
+
: options.reduce((acc, item) => acc.concat(item), [])
|
|
60
62
|
return flat.map((opt, i) => {
|
|
61
63
|
if (typeof opt === 'string') return { id: String(i + 1), text: opt, selected: false }
|
|
62
64
|
const id = String(opt?.id ?? opt?.buttonId ?? i + 1).trim()
|
package/lib/documents.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { open } from 'node:fs/promises'
|
|
2
2
|
import { extname } from 'node:path'
|
|
3
3
|
import { inflateRawSync } from 'node:zlib'
|
|
4
4
|
|
|
@@ -132,7 +132,36 @@ export async function parseDocument(filePath, opts = {}) {
|
|
|
132
132
|
const ext = extname(filePath).toLowerCase()
|
|
133
133
|
|
|
134
134
|
try {
|
|
135
|
-
const
|
|
135
|
+
const handle = await open(filePath, 'r')
|
|
136
|
+
let rawBuffer
|
|
137
|
+
let truncated = false
|
|
138
|
+
let originalLength = 0
|
|
139
|
+
|
|
140
|
+
try {
|
|
141
|
+
const st = await handle.stat()
|
|
142
|
+
originalLength = st.size
|
|
143
|
+
|
|
144
|
+
if (TEXT_INJECT_EXTS.has(ext)) {
|
|
145
|
+
const toRead = Math.min(st.size, maxBytes)
|
|
146
|
+
const buf = Buffer.alloc(toRead)
|
|
147
|
+
const { bytesRead } = await handle.read(buf, 0, toRead, 0)
|
|
148
|
+
rawBuffer = buf.subarray(0, bytesRead)
|
|
149
|
+
truncated = st.size > toRead
|
|
150
|
+
} else if (ext === '.pdf' || ext === '.docx') {
|
|
151
|
+
const maxBinary = Math.max(maxBytes * 4, 10 * 1024 * 1024)
|
|
152
|
+
if (st.size > maxBinary) {
|
|
153
|
+
return { parsed: false, error: `File size (${st.size} bytes) exceeds safety parse limit` }
|
|
154
|
+
}
|
|
155
|
+
const buf = Buffer.alloc(st.size)
|
|
156
|
+
const { bytesRead } = await handle.read(buf, 0, st.size, 0)
|
|
157
|
+
rawBuffer = buf.subarray(0, bytesRead)
|
|
158
|
+
} else {
|
|
159
|
+
return { parsed: false, type: 'unsupported' }
|
|
160
|
+
}
|
|
161
|
+
} finally {
|
|
162
|
+
await handle.close()
|
|
163
|
+
}
|
|
164
|
+
|
|
136
165
|
let text = ''
|
|
137
166
|
let parsedType = 'text'
|
|
138
167
|
|
|
@@ -145,15 +174,11 @@ export async function parseDocument(filePath, opts = {}) {
|
|
|
145
174
|
} else if (TEXT_INJECT_EXTS.has(ext)) {
|
|
146
175
|
parsedType = ext.slice(1)
|
|
147
176
|
text = rawBuffer.toString('utf8')
|
|
148
|
-
} else {
|
|
149
|
-
return { parsed: false, type: 'unsupported' }
|
|
150
177
|
}
|
|
151
178
|
|
|
152
179
|
text = text.trim()
|
|
153
180
|
if (!text) return { parsed: false, type: parsedType }
|
|
154
181
|
|
|
155
|
-
const originalLength = text.length
|
|
156
|
-
let truncated = false
|
|
157
182
|
if (text.length > maxBytes) {
|
|
158
183
|
text = text.slice(0, maxBytes)
|
|
159
184
|
truncated = true
|
|
@@ -164,9 +189,10 @@ export async function parseDocument(filePath, opts = {}) {
|
|
|
164
189
|
type: parsedType,
|
|
165
190
|
text,
|
|
166
191
|
truncated,
|
|
167
|
-
originalLength,
|
|
192
|
+
originalLength: originalLength || text.length,
|
|
168
193
|
}
|
|
169
194
|
} catch (err) {
|
|
170
195
|
return { parsed: false, error: err.message }
|
|
171
196
|
}
|
|
172
197
|
}
|
|
198
|
+
|
package/lib/messenger-api.js
CHANGED
|
@@ -110,7 +110,29 @@ export function normalizeFiles(files) {
|
|
|
110
110
|
err.status = 400
|
|
111
111
|
throw err
|
|
112
112
|
}
|
|
113
|
-
if (file.bytes)
|
|
113
|
+
if (file.bytes !== undefined) {
|
|
114
|
+
let buf = null
|
|
115
|
+
if (Buffer.isBuffer(file.bytes) || file.bytes instanceof Uint8Array) {
|
|
116
|
+
buf = Buffer.from(file.bytes)
|
|
117
|
+
} else if (Array.isArray(file.bytes)) {
|
|
118
|
+
buf = Buffer.from(file.bytes)
|
|
119
|
+
} else if (file.bytes && typeof file.bytes === 'object' && file.bytes.type === 'Buffer' && Array.isArray(file.bytes.data)) {
|
|
120
|
+
buf = Buffer.from(file.bytes.data)
|
|
121
|
+
} else if (typeof file.bytes === 'string') {
|
|
122
|
+
buf = Buffer.from(file.bytes, 'utf8')
|
|
123
|
+
}
|
|
124
|
+
if (!buf) {
|
|
125
|
+
const err = new Error(`files[${i}].bytes must be a Buffer, Uint8Array, or array of bytes`)
|
|
126
|
+
err.status = 400
|
|
127
|
+
throw err
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
bytes: buf,
|
|
131
|
+
mime: file.mime || 'application/octet-stream',
|
|
132
|
+
kind: file.kind || 'document',
|
|
133
|
+
name: file.name || 'file',
|
|
134
|
+
}
|
|
135
|
+
}
|
|
114
136
|
if (typeof file.dataBase64 === 'string') {
|
|
115
137
|
return {
|
|
116
138
|
bytes: Buffer.from(file.dataBase64, 'base64'),
|
package/lib/outbound.js
CHANGED
|
@@ -72,12 +72,54 @@ export async function readAttachmentBytes(ctx, ref) {
|
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
export async function fetchInternalImage(baseUrl, path, signal) {
|
|
75
|
+
export async function fetchInternalImage(baseUrl, path, signal, opts = {}) {
|
|
76
|
+
const { timeoutMs = 15_000, maxBytes = 20 * 1024 * 1024 } = opts
|
|
77
|
+
const timeoutSignal = AbortSignal.timeout(timeoutMs)
|
|
78
|
+
const effectiveSignal = signal
|
|
79
|
+
? (typeof AbortSignal.any === 'function' ? AbortSignal.any([signal, timeoutSignal]) : timeoutSignal)
|
|
80
|
+
: timeoutSignal
|
|
81
|
+
|
|
76
82
|
const url = new URL(path, baseUrl.endsWith('/') ? baseUrl : `${baseUrl}/`)
|
|
77
|
-
const res = await fetch(url, { signal })
|
|
83
|
+
const res = await fetch(url, { signal: effectiveSignal })
|
|
78
84
|
if (!res.ok) throw new Error(`image fetch HTTP ${res.status}`)
|
|
79
|
-
|
|
80
|
-
const
|
|
85
|
+
|
|
86
|
+
const clHeader = res.headers?.get?.('content-length')
|
|
87
|
+
const cl = Number(clHeader)
|
|
88
|
+
if (Number.isFinite(cl) && cl > maxBytes) {
|
|
89
|
+
throw new Error(`image size (${cl} bytes) exceeds limit of ${maxBytes} bytes`)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let bytes
|
|
93
|
+
if (res.body && typeof res.body.getReader === 'function') {
|
|
94
|
+
const reader = res.body.getReader()
|
|
95
|
+
const chunks = []
|
|
96
|
+
let total = 0
|
|
97
|
+
while (true) {
|
|
98
|
+
const { done, value } = await reader.read()
|
|
99
|
+
if (done) break
|
|
100
|
+
total += value.length
|
|
101
|
+
if (total > maxBytes) {
|
|
102
|
+
reader.cancel().catch(() => {})
|
|
103
|
+
throw new Error(`image size exceeds limit of ${maxBytes} bytes`)
|
|
104
|
+
}
|
|
105
|
+
chunks.push(value)
|
|
106
|
+
}
|
|
107
|
+
const combined = new Uint8Array(total)
|
|
108
|
+
let offset = 0
|
|
109
|
+
for (const chunk of chunks) {
|
|
110
|
+
combined.set(chunk, offset)
|
|
111
|
+
offset += chunk.length
|
|
112
|
+
}
|
|
113
|
+
bytes = combined
|
|
114
|
+
} else {
|
|
115
|
+
const ab = await res.arrayBuffer()
|
|
116
|
+
if (ab.byteLength > maxBytes) {
|
|
117
|
+
throw new Error(`image size exceeds limit of ${maxBytes} bytes`)
|
|
118
|
+
}
|
|
119
|
+
bytes = new Uint8Array(ab)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const mime = res.headers?.get?.('content-type') || 'image/png'
|
|
81
123
|
const fromQuery = url.searchParams.get('id') || url.searchParams.get('name')
|
|
82
124
|
const name = fromQuery ? `image-${fromQuery.slice(0, 32)}.png` : 'image.png'
|
|
83
125
|
return { bytes, mime, kind: 'photo', name }
|
|
@@ -87,7 +129,7 @@ const DEFAULT_MAX_OUTBOUND_FILES = 10
|
|
|
87
129
|
|
|
88
130
|
/** Build Telegram-ready files from assistant image blocks and inline plugin URLs. */
|
|
89
131
|
export async function buildOutboundFiles(ctx, baseUrl, collector, opts = {}) {
|
|
90
|
-
const { signal, logger, maxFiles = DEFAULT_MAX_OUTBOUND_FILES } = opts
|
|
132
|
+
const { signal, logger, maxFiles = DEFAULT_MAX_OUTBOUND_FILES, maxImageBytes, imageTimeoutMs } = opts
|
|
91
133
|
const files = []
|
|
92
134
|
const refs = dedupeAttachmentRefs(collector.images)
|
|
93
135
|
for (const ref of refs) {
|
|
@@ -104,7 +146,10 @@ export async function buildOutboundFiles(ctx, baseUrl, collector, opts = {}) {
|
|
|
104
146
|
if (files.length >= maxFiles) break
|
|
105
147
|
if (signal?.aborted) break
|
|
106
148
|
try {
|
|
107
|
-
files.push(await fetchInternalImage(baseUrl, path, signal
|
|
149
|
+
files.push(await fetchInternalImage(baseUrl, path, signal, {
|
|
150
|
+
maxBytes: maxImageBytes,
|
|
151
|
+
timeoutMs: imageTimeoutMs,
|
|
152
|
+
}))
|
|
108
153
|
} catch (e) {
|
|
109
154
|
logger?.warn?.(`outbound url image: ${e.message}`)
|
|
110
155
|
}
|
package/lib/personas.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { readFileSync
|
|
2
|
-
import {
|
|
1
|
+
import { readFileSync } from 'node:fs'
|
|
2
|
+
import { writeJsonAtomicSync } from './storage-atomic.js'
|
|
3
3
|
|
|
4
4
|
export const BUILTIN_PERSONAS = {
|
|
5
5
|
default: {
|
|
@@ -87,8 +87,7 @@ export function createPersonaStore(filePath, { logger } = {}) {
|
|
|
87
87
|
|
|
88
88
|
function save() {
|
|
89
89
|
try {
|
|
90
|
-
|
|
91
|
-
writeFileSync(filePath, JSON.stringify(cache, null, 2), 'utf8')
|
|
90
|
+
writeJsonAtomicSync(filePath, cache)
|
|
92
91
|
} catch (err) {
|
|
93
92
|
logger?.warn?.('[dsh-messenger-gateway] personas save error:', err?.message || err)
|
|
94
93
|
}
|
package/lib/scheduler.js
CHANGED
|
@@ -42,10 +42,16 @@ export function createScheduler(filePath, onDue, options = {}) {
|
|
|
42
42
|
async function load() {
|
|
43
43
|
try {
|
|
44
44
|
const raw = await readFile(filePath, 'utf8')
|
|
45
|
-
|
|
45
|
+
const parsed = JSON.parse(raw)
|
|
46
|
+
if (Array.isArray(parsed)) {
|
|
47
|
+
tasks = parsed
|
|
48
|
+
} else {
|
|
49
|
+
logger?.warn?.(`[dsh-messenger-gateway] scheduler invalid tasks payload in ${filePath}: expected array, got ${typeof parsed}; resetting to empty list`)
|
|
50
|
+
tasks = []
|
|
51
|
+
}
|
|
46
52
|
} catch (err) {
|
|
47
53
|
if (err?.code !== 'ENOENT') {
|
|
48
|
-
logger?.
|
|
54
|
+
logger?.warn?.('[dsh-messenger-gateway] scheduler load error:', err?.message || err)
|
|
49
55
|
}
|
|
50
56
|
tasks = []
|
|
51
57
|
}
|
|
@@ -56,6 +62,7 @@ export function createScheduler(filePath, onDue, options = {}) {
|
|
|
56
62
|
|
|
57
63
|
async function save() {
|
|
58
64
|
if (!filePath) return
|
|
65
|
+
if (!Array.isArray(tasks)) tasks = []
|
|
59
66
|
try {
|
|
60
67
|
const now = Date.now()
|
|
61
68
|
tasks = tasks.filter((t) => {
|
|
@@ -71,6 +78,7 @@ export function createScheduler(filePath, onDue, options = {}) {
|
|
|
71
78
|
|
|
72
79
|
async function checkDue() {
|
|
73
80
|
if (!loaded) await load()
|
|
81
|
+
if (!Array.isArray(tasks)) tasks = []
|
|
74
82
|
const now = Date.now()
|
|
75
83
|
const due = tasks.filter((t) => t.status === 'pending' && t.dueAt <= now)
|
|
76
84
|
|
|
@@ -99,10 +107,10 @@ export function createScheduler(filePath, onDue, options = {}) {
|
|
|
99
107
|
function start(intervalMs = 5000) {
|
|
100
108
|
if (timer) clearInterval(timer)
|
|
101
109
|
timer = setInterval(() => {
|
|
102
|
-
checkDue().catch((err) => { logger?.
|
|
110
|
+
checkDue().catch((err) => { logger?.warn?.('[dsh-messenger-gateway] scheduler checkDue error:', err?.message || err) })
|
|
103
111
|
}, intervalMs)
|
|
104
112
|
timer.unref?.()
|
|
105
|
-
checkDue().catch((err) => { logger?.
|
|
113
|
+
checkDue().catch((err) => { logger?.warn?.('[dsh-messenger-gateway] scheduler checkDue error:', err?.message || err) })
|
|
106
114
|
}
|
|
107
115
|
|
|
108
116
|
function stop() {
|
package/lib/settings-bridge.js
CHANGED
|
@@ -78,7 +78,7 @@ export function setupSettings(ctx, { namespace, Config, resolveConfig, baseConfi
|
|
|
78
78
|
source = () => currentConfig
|
|
79
79
|
|
|
80
80
|
const persist = async (next) => {
|
|
81
|
-
const payload =
|
|
81
|
+
const payload = resolveConfig(next)
|
|
82
82
|
if (typeof service.replace === 'function') {
|
|
83
83
|
await service.replace(namespace, payload, getRevision())
|
|
84
84
|
} else if (typeof service.update === 'function') {
|
|
@@ -88,7 +88,7 @@ export function setupSettings(ctx, { namespace, Config, resolveConfig, baseConfi
|
|
|
88
88
|
} else {
|
|
89
89
|
throw new Error('settings service cannot persist configuration')
|
|
90
90
|
}
|
|
91
|
-
currentConfig =
|
|
91
|
+
currentConfig = payload
|
|
92
92
|
sync()
|
|
93
93
|
for (const cb of watchers) {
|
|
94
94
|
try { cb(currentConfig) } catch (err) { logger?.debug?.(`${namespace}: watcher error:`, err?.message || err) }
|
package/lib/storage-atomic.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { writeFile, rename, mkdir, unlink } from 'node:fs/promises'
|
|
2
|
+
import { writeFileSync, renameSync, mkdirSync, unlinkSync } from 'node:fs'
|
|
2
3
|
import { dirname } from 'node:path'
|
|
3
4
|
import { randomUUID } from 'node:crypto'
|
|
4
5
|
|
|
@@ -18,4 +19,20 @@ export async function writeJsonAtomic(filePath, data) {
|
|
|
18
19
|
await unlink(tmpPath).catch(() => {})
|
|
19
20
|
throw err
|
|
20
21
|
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Synchronous atomic JSON write via temporary file and rename.
|
|
26
|
+
*/
|
|
27
|
+
export function writeJsonAtomicSync(filePath, data) {
|
|
28
|
+
const dir = dirname(filePath)
|
|
29
|
+
mkdirSync(dir, { recursive: true })
|
|
30
|
+
const tmpPath = `${filePath}.${randomUUID().slice(0, 8)}.tmp`
|
|
31
|
+
try {
|
|
32
|
+
writeFileSync(tmpPath, JSON.stringify(data, null, 2), 'utf8')
|
|
33
|
+
renameSync(tmpPath, filePath)
|
|
34
|
+
} catch (err) {
|
|
35
|
+
try { unlinkSync(tmpPath) } catch { /* ignore */ }
|
|
36
|
+
throw err
|
|
37
|
+
}
|
|
21
38
|
}
|
package/lib/updater.js
CHANGED
|
@@ -181,15 +181,20 @@ export async function getUpdateStatus(options, target = runtime()) {
|
|
|
181
181
|
}
|
|
182
182
|
}
|
|
183
183
|
|
|
184
|
+
export function buildUpdateArgs(target, packageSpec, options = {}) {
|
|
185
|
+
return [
|
|
186
|
+
target.cliEntry, 'plugin', '--profile', target.profileName, 'add',
|
|
187
|
+
packageSpec,
|
|
188
|
+
`--registry=${options.registry ?? 'https://registry.npmjs.org/'}`,
|
|
189
|
+
]
|
|
190
|
+
}
|
|
191
|
+
|
|
184
192
|
export async function installExact(target, packageSpec, options = {}) {
|
|
185
193
|
if (target.cliEntry === undefined) throw new Error('Automatic update CLI entry is unavailable in this runtime.')
|
|
186
194
|
return new Promise((resolvePromise, reject) => {
|
|
187
|
-
const args =
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
`--registry=${options.registry ?? 'https://registry.npmjs.org/'}`,
|
|
191
|
-
]
|
|
192
|
-
const child = spawn(process.execPath, args, {
|
|
195
|
+
const args = buildUpdateArgs(target, packageSpec, options)
|
|
196
|
+
const spawnFn = options.spawn ?? spawn
|
|
197
|
+
const child = spawnFn(process.execPath, args, {
|
|
193
198
|
cwd: target.profileDir,
|
|
194
199
|
windowsHide: true,
|
|
195
200
|
stdio: ['ignore', 'pipe', 'pipe'],
|
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.11",
|
|
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",
|