@goodandready/dsh-messenger-gateway 0.3.19 → 0.3.21

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/scheduler.js CHANGED
@@ -1,143 +1,160 @@
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) {
24
- if (ms <= 0) return 'сейчас'
25
- const sec = Math.ceil(ms / 1000)
26
- if (sec < 60) return `${sec} сек`
27
- const min = Math.ceil(sec / 60)
28
- if (min < 60) return `${min} мин`
29
- const hr = Math.floor(min / 60)
30
- const remMin = min % 60
31
- return remMin ? `${hr} ч ${remMin} мин` : `${hr} ч`
32
- }
33
-
34
- export function createScheduler(filePath, onDue) {
35
- let tasks = []
36
- let loaded = false
37
- let timer = null
38
-
39
- async function load() {
40
- try {
41
- const raw = await readFile(filePath, 'utf8')
42
- tasks = JSON.parse(raw)
43
- } catch {
44
- tasks = []
45
- }
46
- loaded = true
47
- }
48
-
49
- const RETENTION_MS = 7 * 86400 * 1000
50
-
51
- async function save() {
52
- if (!filePath) return
53
- try {
54
- const now = Date.now()
55
- tasks = tasks.filter((t) => {
56
- if (t.status === 'pending') return true
57
- const finishTime = t.firedAt || t.createdAt || 0
58
- return (now - finishTime) < RETENTION_MS
59
- })
60
- await writeJsonAtomic(filePath, tasks)
61
- } catch {}
62
- }
63
-
64
- async function checkDue() {
65
- if (!loaded) await load()
66
- const now = Date.now()
67
- const due = tasks.filter((t) => t.status === 'pending' && t.dueAt <= now)
68
-
69
- for (const task of due) {
70
- task.status = 'fired'
71
- task.firedAt = now
72
- if (typeof onDue === 'function') {
73
- try { await onDue(task) } catch {}
74
- }
75
- }
76
-
77
- if (due.length > 0) {
78
- await save()
79
- }
80
- }
81
-
82
- function start(intervalMs = 5000) {
83
- if (timer) clearInterval(timer)
84
- timer = setInterval(() => {
85
- checkDue().catch(() => {})
86
- }, intervalMs)
87
- timer.unref?.()
88
- checkDue().catch(() => {})
89
- }
90
-
91
- function stop() {
92
- if (timer) {
93
- clearInterval(timer)
94
- timer = null
95
- }
96
- }
97
-
98
- async function schedule(taskData) {
99
- if (!loaded) await load()
100
- const id = taskData.id || randomUUID().slice(0, 8)
101
- const task = {
102
- id,
103
- platform: taskData.platform || 'telegram',
104
- chatId: taskData.chatId,
105
- threadId: taskData.threadId || 0,
106
- userId: taskData.userId,
107
- text: taskData.text,
108
- dueAt: taskData.dueAt,
109
- createdAt: Date.now(),
110
- status: 'pending',
111
- }
112
- tasks.push(task)
113
- await save()
114
- return task
115
- }
116
-
117
- async function list(chatId) {
118
- if (!loaded) await load()
119
- const now = Date.now()
120
- return tasks.filter((t) => (chatId ? t.chatId === chatId : true) && t.status === 'pending' && t.dueAt > now)
121
- }
122
-
123
- async function cancel(id, chatId) {
124
- if (!loaded) await load()
125
- const task = tasks.find((t) => t.id === id && (chatId ? t.chatId === chatId : true) && t.status === 'pending')
126
- if (!task) return false
127
- task.status = 'cancelled'
128
- await save()
129
- return true
130
- }
131
-
132
- return {
133
- load,
134
- save,
135
- start,
136
- stop,
137
- schedule,
138
- list,
139
- cancel,
140
- checkDue,
141
- getTasks: () => tasks,
142
- }
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
+ }
143
160
  }
package/lib/stream.js CHANGED
@@ -11,8 +11,10 @@ export function extractToolName(eventData) {
11
11
  return name ? String(name) : ''
12
12
  }
13
13
 
14
- export function formatProgressLine(toolName) {
15
- if (!toolName) return '⏳ Думаю…'
14
+ export function formatProgressLine(toolName, locale = 'en') {
15
+ if (!toolName) {
16
+ return String(locale || '').toLowerCase().startsWith('zh') ? '⏳ 思考中…' : '⏳ Thinking…'
17
+ }
16
18
  return `🔧 ${toolName}…`
17
19
  }
18
20
 
File without changes
package/lib/text.js CHANGED
@@ -1,39 +1,39 @@
1
- export function splitText(text, maxLen) {
2
- const raw = String(text ?? '')
3
- if (raw.length <= maxLen) return [raw]
4
- const chunks = []
5
- let rest = raw
6
- while (rest.length > maxLen) {
7
- let cut = rest.lastIndexOf('\n', maxLen)
8
- if (cut < maxLen / 2) cut = rest.lastIndexOf(' ', maxLen)
9
- if (cut < maxLen / 2) cut = maxLen
10
- chunks.push(rest.slice(0, cut).trim())
11
- rest = rest.slice(cut).trimStart()
12
- }
13
- if (rest.length > 0) chunks.push(rest)
14
- return chunks
15
- }
16
-
17
- export function assistantText(message) {
18
- return (message?.content || [])
19
- .filter((block) => block.type === 'text')
20
- .map((block) => block.text)
21
- .join('')
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. Отвечай только пользователю по-русски. Не выводи ход рассуждений, планирование и служебный текст на английском.'
1
+ export function splitText(text, maxLen) {
2
+ const raw = String(text ?? '')
3
+ if (raw.length <= maxLen) return [raw]
4
+ const chunks = []
5
+ let rest = raw
6
+ while (rest.length > maxLen) {
7
+ let cut = rest.lastIndexOf('\n', maxLen)
8
+ if (cut < maxLen / 2) cut = rest.lastIndexOf(' ', maxLen)
9
+ if (cut < maxLen / 2) cut = maxLen
10
+ chunks.push(rest.slice(0, cut).trim())
11
+ rest = rest.slice(cut).trimStart()
12
+ }
13
+ if (rest.length > 0) chunks.push(rest)
14
+ return chunks
15
+ }
16
+
17
+ export function assistantText(message) {
18
+ return (message?.content || [])
19
+ .filter((block) => block.type === 'text')
20
+ .map((block) => block.text)
21
+ .join('')
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 = 'Channel: Messenger. Reply concisely and directly to the user. Do not leak internal reasoning or planning traces.'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@goodandready/dsh-messenger-gateway",
3
- "version": "0.3.19",
3
+ "version": "0.3.21",
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",