@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/README.md +40 -2
- package/README.ru.md +10 -0
- package/README.zh.md +10 -0
- package/lib/adapters/discord.js +65 -11
- package/lib/adapters/slack.js +72 -12
- package/lib/adapters/telegram.js +708 -690
- package/lib/alerts.js +10 -10
- package/lib/artifacts.js +117 -117
- package/lib/ask.js +151 -151
- package/lib/client.js +104 -104
- package/lib/commands.js +73 -47
- package/lib/config.js +104 -98
- package/lib/content-guard.js +14 -14
- package/lib/documents.js +9 -9
- package/lib/file-manager.js +9 -9
- package/lib/gateway.js +1737 -1406
- package/lib/index.js +500 -488
- package/lib/locales/en.js +114 -0
- package/lib/locales/index.js +21 -0
- package/lib/locales/zh.js +114 -0
- package/lib/models.js +1 -1
- package/lib/personas.js +67 -14
- package/lib/photos.js +2 -2
- package/lib/scheduler.js +159 -142
- package/lib/stream.js +4 -2
- package/lib/telegram-errors.js +0 -0
- package/lib/text.js +39 -39
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -1,488 +1,500 @@
|
|
|
1
|
-
import { homedir } from 'node:os'
|
|
2
|
-
import { join } from 'node:path'
|
|
3
|
-
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
4
|
-
import { Gateway } from './gateway.js'
|
|
5
|
-
import { readBody, writeJson, isTrustedSettingsRequest } from './http.js'
|
|
6
|
-
import {
|
|
7
|
-
createMessengerService, dispatchMessenger, httpStatusForError,
|
|
8
|
-
messengerApiSchema, parseMessengerBody,
|
|
9
|
-
} from './messenger-api.js'
|
|
10
|
-
import { listHomes } from './homes.js'
|
|
11
|
-
import { Config } from './config.js'
|
|
12
|
-
|
|
13
|
-
export const name = 'dsh-messenger-gateway'
|
|
14
|
-
export const inject = ['agentDefaultModel', 'agents', 'sessions', 'loader', 'settings', 'webServer', 'attachments', 'tools']
|
|
15
|
-
|
|
16
|
-
export const SETTINGS_NAMESPACE = 'dsh-messenger-gateway'
|
|
17
|
-
export { Config }
|
|
18
|
-
|
|
19
|
-
function resolveConfig(raw) {
|
|
20
|
-
const cfg = Config(raw)
|
|
21
|
-
const home = process.env.DSH_HOME || join(homedir(), '.dsh')
|
|
22
|
-
if (!cfg.media.cacheDir) {
|
|
23
|
-
cfg.media = { ...cfg.media, cacheDir: join(home, 'messenger-gateway', 'cache') }
|
|
24
|
-
}
|
|
25
|
-
return cfg
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function publicConfig(cfg) {
|
|
29
|
-
return {
|
|
30
|
-
enabled: cfg.enabled,
|
|
31
|
-
internalBaseURL: cfg.internalBaseURL,
|
|
32
|
-
telegram: {
|
|
33
|
-
enabled: cfg.telegram.enabled,
|
|
34
|
-
allowedUserIds: cfg.telegram.allowedUserIds,
|
|
35
|
-
pollTimeoutSeconds: cfg.telegram.pollTimeoutSeconds,
|
|
36
|
-
pollIntervalMs: cfg.telegram.pollIntervalMs,
|
|
37
|
-
commands: cfg.telegram.commands,
|
|
38
|
-
textFormat: cfg.telegram.textFormat,
|
|
39
|
-
homeChatId: cfg.telegram.homeChatId,
|
|
40
|
-
homeThreadId: cfg.telegram.homeThreadId,
|
|
41
|
-
homes: listHomes(cfg.telegram),
|
|
42
|
-
pairingEnabled: cfg.telegram.pairingEnabled,
|
|
43
|
-
streaming: cfg.telegram.streaming,
|
|
44
|
-
progressEnabled: cfg.telegram.progressEnabled,
|
|
45
|
-
approvalsEnabled: cfg.telegram.approvalsEnabled,
|
|
46
|
-
groupsEnabled: cfg.telegram.groupsEnabled,
|
|
47
|
-
groupRequireMention: cfg.telegram.groupRequireMention,
|
|
48
|
-
reactionsEnabled: cfg.telegram.reactionsEnabled,
|
|
49
|
-
transport: cfg.telegram.transport,
|
|
50
|
-
webhookUrl: cfg.telegram.webhookUrl,
|
|
51
|
-
webhookPath: cfg.telegram.webhookPath,
|
|
52
|
-
webhookSecretConfigured: Boolean(String(cfg.telegram.webhookSecret || '').trim()),
|
|
53
|
-
botTokenConfigured: Boolean(String(cfg.telegram.botToken || '').trim()),
|
|
54
|
-
voiceMode: cfg.telegram.voiceMode,
|
|
55
|
-
quickActions: cfg.telegram.quickActions !== false,
|
|
56
|
-
artifactPreviews: cfg.telegram.artifactPreviews !== false,
|
|
57
|
-
notifyBridge: {
|
|
58
|
-
enabled: Boolean(cfg.telegram.notifyBridge?.enabled),
|
|
59
|
-
events: cfg.telegram.notifyBridge?.events || ['task_done', 'error'],
|
|
60
|
-
home: cfg.telegram.notifyBridge?.home || 'default',
|
|
61
|
-
excludeSessionPrefixes: cfg.telegram.notifyBridge?.excludeSessionPrefixes || ['msgw-'],
|
|
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
|
-
writeJson(res,
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
let
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
if (
|
|
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
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
return { ok:
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
if (
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
if (
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
const
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
writeJson(res,
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
if (
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
if (!
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
}
|
|
1
|
+
import { homedir } from 'node:os'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
4
|
+
import { Gateway } from './gateway.js'
|
|
5
|
+
import { readBody, writeJson, isTrustedSettingsRequest } from './http.js'
|
|
6
|
+
import {
|
|
7
|
+
createMessengerService, dispatchMessenger, httpStatusForError,
|
|
8
|
+
messengerApiSchema, parseMessengerBody,
|
|
9
|
+
} from './messenger-api.js'
|
|
10
|
+
import { listHomes } from './homes.js'
|
|
11
|
+
import { Config } from './config.js'
|
|
12
|
+
|
|
13
|
+
export const name = 'dsh-messenger-gateway'
|
|
14
|
+
export const inject = ['agentDefaultModel', 'agents', 'sessions', 'loader', 'settings', 'webServer', 'attachments', 'tools']
|
|
15
|
+
|
|
16
|
+
export const SETTINGS_NAMESPACE = 'dsh-messenger-gateway'
|
|
17
|
+
export { Config }
|
|
18
|
+
|
|
19
|
+
function resolveConfig(raw) {
|
|
20
|
+
const cfg = Config(raw)
|
|
21
|
+
const home = process.env.DSH_HOME || join(homedir(), '.dsh')
|
|
22
|
+
if (!cfg.media.cacheDir) {
|
|
23
|
+
cfg.media = { ...cfg.media, cacheDir: join(home, 'messenger-gateway', 'cache') }
|
|
24
|
+
}
|
|
25
|
+
return cfg
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function publicConfig(cfg) {
|
|
29
|
+
return {
|
|
30
|
+
enabled: cfg.enabled,
|
|
31
|
+
internalBaseURL: cfg.internalBaseURL,
|
|
32
|
+
telegram: {
|
|
33
|
+
enabled: cfg.telegram.enabled,
|
|
34
|
+
allowedUserIds: cfg.telegram.allowedUserIds,
|
|
35
|
+
pollTimeoutSeconds: cfg.telegram.pollTimeoutSeconds,
|
|
36
|
+
pollIntervalMs: cfg.telegram.pollIntervalMs,
|
|
37
|
+
commands: cfg.telegram.commands,
|
|
38
|
+
textFormat: cfg.telegram.textFormat,
|
|
39
|
+
homeChatId: cfg.telegram.homeChatId,
|
|
40
|
+
homeThreadId: cfg.telegram.homeThreadId,
|
|
41
|
+
homes: listHomes(cfg.telegram),
|
|
42
|
+
pairingEnabled: cfg.telegram.pairingEnabled,
|
|
43
|
+
streaming: cfg.telegram.streaming,
|
|
44
|
+
progressEnabled: cfg.telegram.progressEnabled,
|
|
45
|
+
approvalsEnabled: cfg.telegram.approvalsEnabled,
|
|
46
|
+
groupsEnabled: cfg.telegram.groupsEnabled,
|
|
47
|
+
groupRequireMention: cfg.telegram.groupRequireMention,
|
|
48
|
+
reactionsEnabled: cfg.telegram.reactionsEnabled,
|
|
49
|
+
transport: cfg.telegram.transport,
|
|
50
|
+
webhookUrl: cfg.telegram.webhookUrl,
|
|
51
|
+
webhookPath: cfg.telegram.webhookPath,
|
|
52
|
+
webhookSecretConfigured: Boolean(String(cfg.telegram.webhookSecret || '').trim()),
|
|
53
|
+
botTokenConfigured: Boolean(String(cfg.telegram.botToken || '').trim()),
|
|
54
|
+
voiceMode: cfg.telegram.voiceMode,
|
|
55
|
+
quickActions: cfg.telegram.quickActions !== false,
|
|
56
|
+
artifactPreviews: cfg.telegram.artifactPreviews !== false,
|
|
57
|
+
notifyBridge: {
|
|
58
|
+
enabled: Boolean(cfg.telegram.notifyBridge?.enabled),
|
|
59
|
+
events: cfg.telegram.notifyBridge?.events || ['task_done', 'error'],
|
|
60
|
+
home: cfg.telegram.notifyBridge?.home || 'default',
|
|
61
|
+
excludeSessionPrefixes: cfg.telegram.notifyBridge?.excludeSessionPrefixes || ['msgw-'],
|
|
62
|
+
},
|
|
63
|
+
forumMirrorEnabled: Boolean(cfg.telegram.forumMirrorEnabled),
|
|
64
|
+
forumMirrorChatId: cfg.telegram.forumMirrorChatId || '',
|
|
65
|
+
},
|
|
66
|
+
discord: { enabled: cfg.discord.enabled, botTokenConfigured: Boolean(String(cfg.discord.botToken || '').trim()) },
|
|
67
|
+
media: { maxDocBytes: cfg.media.maxDocBytes, maxTextInjectBytes: cfg.media.maxTextInjectBytes, maxImageBytes: cfg.media.maxImageBytes },
|
|
68
|
+
tts: cfg.tts,
|
|
69
|
+
agent: {
|
|
70
|
+
provider: cfg.agent.provider,
|
|
71
|
+
model: cfg.agent.model,
|
|
72
|
+
photoOnlyMode: cfg.agent.photoOnlyMode,
|
|
73
|
+
maxMessageLength: cfg.agent.maxMessageLength,
|
|
74
|
+
turnTimeoutMs: cfg.agent.turnTimeoutMs,
|
|
75
|
+
idleTimeoutMs: cfg.agent.idleTimeoutMs,
|
|
76
|
+
instructionPrefix: cfg.agent.instructionPrefix,
|
|
77
|
+
sessionScope: cfg.agent.sessionScope,
|
|
78
|
+
},
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function registerMessengerRoute(ctx, getGw, path, action) {
|
|
83
|
+
ctx.effect(() => (ctx.get?.('webServer') || ctx.webServer).register({
|
|
84
|
+
kind: 'exact', path,
|
|
85
|
+
handler: async (req, res) => {
|
|
86
|
+
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
|
|
87
|
+
let payload
|
|
88
|
+
try {
|
|
89
|
+
payload = parseMessengerBody((await readBody(req)).toString('utf8'))
|
|
90
|
+
} catch (err) {
|
|
91
|
+
return writeJson(res, httpStatusForError(err), { ok: false, error: err.message })
|
|
92
|
+
}
|
|
93
|
+
try {
|
|
94
|
+
const out = await dispatchMessenger(getGw(), action, payload)
|
|
95
|
+
writeJson(res, 200, out)
|
|
96
|
+
} catch (err) {
|
|
97
|
+
writeJson(res, httpStatusForError(err), { ok: false, error: err.message })
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
}), `dsh-messenger-gateway: ${action}`)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function isExcludedSession(sessionId, prefixes) {
|
|
104
|
+
const sid = String(sessionId || '')
|
|
105
|
+
for (const p of prefixes || []) {
|
|
106
|
+
if (typeof p === 'string' && p.length > 0 && sid.startsWith(p)) return true
|
|
107
|
+
}
|
|
108
|
+
return false
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function apply(ctx, config) {
|
|
112
|
+
const entry = resolveConfig(structuredClone(config || {}))
|
|
113
|
+
let gateway
|
|
114
|
+
let source = () => entry
|
|
115
|
+
let settingsApi
|
|
116
|
+
const turnStarts = new Map()
|
|
117
|
+
const getWebServer = () => ctx.get?.('webServer') || ctx.webServer
|
|
118
|
+
|
|
119
|
+
const sync = () => {
|
|
120
|
+
if (gateway) { gateway.stop(); gateway = undefined }
|
|
121
|
+
const effective = source()
|
|
122
|
+
if (effective.enabled === false) return
|
|
123
|
+
gateway = new Gateway(ctx, effective, {
|
|
124
|
+
persistAllowedUserIds: async (ids) => {
|
|
125
|
+
if (!settingsApi) return
|
|
126
|
+
const cur = source()
|
|
127
|
+
await settingsApi.replace(Config({
|
|
128
|
+
...cur,
|
|
129
|
+
telegram: { ...cur.telegram, allowedUserIds: ids },
|
|
130
|
+
}))
|
|
131
|
+
},
|
|
132
|
+
persistHome: async ({ chatId, threadId }) => {
|
|
133
|
+
if (!settingsApi) throw new Error('settings not ready')
|
|
134
|
+
const cur = source()
|
|
135
|
+
await settingsApi.replace(Config({
|
|
136
|
+
...cur,
|
|
137
|
+
telegram: { ...cur.telegram, homeChatId: chatId, homeThreadId: threadId || 0 },
|
|
138
|
+
}))
|
|
139
|
+
},
|
|
140
|
+
persistHomes: async (nextTelegram) => {
|
|
141
|
+
if (!settingsApi) throw new Error('settings not ready')
|
|
142
|
+
const cur = source()
|
|
143
|
+
await settingsApi.replace(Config({
|
|
144
|
+
...cur,
|
|
145
|
+
telegram: { ...cur.telegram, ...nextTelegram },
|
|
146
|
+
}))
|
|
147
|
+
},
|
|
148
|
+
persistAgentModel: async ({ provider, model }) => {
|
|
149
|
+
if (!settingsApi) return
|
|
150
|
+
const cur = source()
|
|
151
|
+
await settingsApi.replace(Config({
|
|
152
|
+
...cur,
|
|
153
|
+
agent: { ...cur.agent, provider, model },
|
|
154
|
+
}))
|
|
155
|
+
},
|
|
156
|
+
})
|
|
157
|
+
gateway.start().catch((err) => ctx.logger?.warn?.(`dsh-messenger-gateway: ${err.message}`))
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const settings = ctx.get?.('settings') || ctx.settings
|
|
161
|
+
if (settings?.register) {
|
|
162
|
+
settingsApi = settings.register(SETTINGS_NAMESPACE, Config, { base: config || {} })
|
|
163
|
+
source = () => resolveConfig(settingsApi.get() ?? config ?? {})
|
|
164
|
+
ctx.effect(() => settingsApi.watch(sync), 'dsh-messenger-gateway: settings')
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const getGw = () => gateway
|
|
168
|
+
|
|
169
|
+
ctx.effect(() => ctx.provide('messenger', createMessengerService(getGw)), 'dsh-messenger-gateway: messenger service')
|
|
170
|
+
|
|
171
|
+
// Agent tool: inline buttons in the telegram chat bound to this msgw session
|
|
172
|
+
const tools = ctx.get?.('tools') || ctx.tools
|
|
173
|
+
if (tools?.register) {
|
|
174
|
+
ctx.effect(() => tools.register(defineTool({
|
|
175
|
+
name: 'messenger_ask',
|
|
176
|
+
description:
|
|
177
|
+
'Ask the Telegram user a single or multiple-choice question with inline buttons/checkboxes and wait for their choice. '
|
|
178
|
+
+ 'Supports mode: "single" (default) or "multi" (checkboxes with Done/Cancel buttons). Only works inside messenger-gateway sessions (msgw-*).',
|
|
179
|
+
parameters: {
|
|
180
|
+
text: { type: 'string', required: true, description: 'Question text shown in Telegram.' },
|
|
181
|
+
buttons: {
|
|
182
|
+
type: 'array',
|
|
183
|
+
description: 'Rows of buttons: [[{ id, text }, ...], ...] (for single choice)',
|
|
184
|
+
},
|
|
185
|
+
options: {
|
|
186
|
+
type: 'array',
|
|
187
|
+
description: 'List of options: [{ id, text, selected?: boolean }, ...] (for single or multi choice)',
|
|
188
|
+
},
|
|
189
|
+
mode: {
|
|
190
|
+
type: 'string',
|
|
191
|
+
description: '"single" for instant choice or "multi" for checkboxes form',
|
|
192
|
+
},
|
|
193
|
+
selected: {
|
|
194
|
+
type: 'array',
|
|
195
|
+
description: 'Initial selected option IDs for multi-select mode',
|
|
196
|
+
},
|
|
197
|
+
pageSize: { type: 'number', description: 'Number of options per page (default 6).' },
|
|
198
|
+
timeoutMs: { type: 'number', description: 'Wait timeout ms (default 300000).' },
|
|
199
|
+
},
|
|
200
|
+
output: {
|
|
201
|
+
schema: {
|
|
202
|
+
type: 'object',
|
|
203
|
+
additionalProperties: false,
|
|
204
|
+
properties: {
|
|
205
|
+
ok: { type: 'boolean' },
|
|
206
|
+
buttonId: { type: 'string' },
|
|
207
|
+
selected: { type: 'array', items: { type: 'string' } },
|
|
208
|
+
data: { type: 'string' },
|
|
209
|
+
error: { type: 'string' },
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
render: (_args, value) => [{
|
|
213
|
+
type: 'text',
|
|
214
|
+
text: value && value.ok
|
|
215
|
+
? (value.selected?.length ? `User selected: ${value.selected.join(', ')}` : `User chose: ${value.buttonId}`)
|
|
216
|
+
: `messenger_ask failed: ${value && value.error ? value.error : 'unknown'}`,
|
|
217
|
+
}],
|
|
218
|
+
},
|
|
219
|
+
execute: async (args, exec) => {
|
|
220
|
+
const gw = getGw()
|
|
221
|
+
if (!gw) return { ok: false, error: 'gateway not running' }
|
|
222
|
+
try {
|
|
223
|
+
const result = await gw.messengerAskFromAgent(exec.agent, {
|
|
224
|
+
text: String(args.text || ''),
|
|
225
|
+
buttons: args.buttons,
|
|
226
|
+
options: args.options,
|
|
227
|
+
mode: args.mode,
|
|
228
|
+
selected: args.selected,
|
|
229
|
+
pageSize: args.pageSize,
|
|
230
|
+
}, Number(args.timeoutMs) || 300_000)
|
|
231
|
+
return { ok: true, buttonId: result?.buttonId, selected: result?.selected, data: result?.data }
|
|
232
|
+
} catch (err) {
|
|
233
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) }
|
|
234
|
+
}
|
|
235
|
+
},
|
|
236
|
+
})), 'dsh-messenger-gateway: messenger_ask tool')
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Dynamic skills sync
|
|
240
|
+
ctx.effect(() => {
|
|
241
|
+
const onSkillsChange = () => {
|
|
242
|
+
const gw = getGw()
|
|
243
|
+
if (gw) gw.syncTelegramCommands().catch(() => {})
|
|
244
|
+
}
|
|
245
|
+
const unbind = ctx.on('skills/change', onSkillsChange)
|
|
246
|
+
return () => { if (typeof unbind === 'function') unbind() }
|
|
247
|
+
}, 'dsh-messenger-gateway: dynamic skills sync')
|
|
248
|
+
|
|
249
|
+
// Notify → Telegram home bridge (non-msgw sessions only)
|
|
250
|
+
ctx.effect(() => ctx.on('session/event', (session, event) => {
|
|
251
|
+
const cfg = source().telegram?.notifyBridge
|
|
252
|
+
if (!cfg?.enabled) return
|
|
253
|
+
if (event.type === 'turn/start') {
|
|
254
|
+
turnStarts.set(String(session.id), Date.now())
|
|
255
|
+
return
|
|
256
|
+
}
|
|
257
|
+
if (event.type !== 'turn/end') return
|
|
258
|
+
const prefixes = cfg.excludeSessionPrefixes || ['msgw-']
|
|
259
|
+
if (isExcludedSession(session.id, prefixes)) return
|
|
260
|
+
const reason = event.data?.reason
|
|
261
|
+
const kind = reason?.kind === 'completed' ? 'task_done' : 'error'
|
|
262
|
+
const events = new Set(cfg.events?.length ? cfg.events : ['task_done', 'error'])
|
|
263
|
+
if (!events.has(kind)) return
|
|
264
|
+
const gw = getGw()
|
|
265
|
+
if (!gw) return
|
|
266
|
+
const started = turnStarts.get(String(session.id))
|
|
267
|
+
turnStarts.delete(String(session.id))
|
|
268
|
+
const dur = started ? Math.round((Date.now() - started) / 1000) : null
|
|
269
|
+
const title = kind === 'task_done' ? 'Task completed' : 'Agent error'
|
|
270
|
+
const lines = [
|
|
271
|
+
`【${title}】`,
|
|
272
|
+
`session: ${session.id}`,
|
|
273
|
+
]
|
|
274
|
+
if (dur != null) lines.push(`duration: ${dur}s`)
|
|
275
|
+
if (reason?.kind === 'error') {
|
|
276
|
+
lines.push(`error: ${reason.error?.message || reason.error?.code || 'unknown'}`)
|
|
277
|
+
}
|
|
278
|
+
const homeName = cfg.home || 'default'
|
|
279
|
+
const home = gw.messenger.home?.() || null
|
|
280
|
+
if (home && gw.isMuted?.(home.chatId)) return
|
|
281
|
+
gw.messengerSend({ platform: 'telegram', home: homeName }, { text: lines.join('\n') })
|
|
282
|
+
.catch((e) => ctx.logger?.warn?.(`notify bridge: ${e.message}`))
|
|
283
|
+
}), 'dsh-messenger-gateway: notify bridge')
|
|
284
|
+
|
|
285
|
+
ctx.effect(() => getWebServer().register({
|
|
286
|
+
kind: 'exact', path: '/dsh-messenger-gateway/status',
|
|
287
|
+
handler: async (req, res) => {
|
|
288
|
+
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET only' })
|
|
289
|
+
const gw = getGw()
|
|
290
|
+
const botInfo = gw?.getBotInfo?.() || {}
|
|
291
|
+
const uptimeSec = gw?.stats?.startedAt ? Math.max(0, Math.round((Date.now() - gw.stats.startedAt) / 1000)) : 0
|
|
292
|
+
writeJson(res, 200, {
|
|
293
|
+
ok: true,
|
|
294
|
+
running: Boolean(gw),
|
|
295
|
+
adapters: gw?.messenger.adapters() || [],
|
|
296
|
+
activeChats: gw?.messenger.activeChats() || 0,
|
|
297
|
+
ttsEnabled: Boolean(source().tts?.enabled),
|
|
298
|
+
messengerService: 'messenger',
|
|
299
|
+
home: gw?.messenger.home?.() || null,
|
|
300
|
+
homes: gw?.messenger.homes?.() || [],
|
|
301
|
+
pairingPending: gw?.messenger.pairingPending?.()?.length || 0,
|
|
302
|
+
botId: botInfo.botId || 0,
|
|
303
|
+
botUsername: botInfo.botUsername || '',
|
|
304
|
+
pollingConflict: Boolean(botInfo.pollingConflict),
|
|
305
|
+
uptimeSec,
|
|
306
|
+
stats: {
|
|
307
|
+
sent: gw?.stats?.sent || 0,
|
|
308
|
+
errors: gw?.stats?.errors || 0,
|
|
309
|
+
startedAt: gw?.stats?.startedAt || 0,
|
|
310
|
+
},
|
|
311
|
+
config: publicConfig(source()),
|
|
312
|
+
})
|
|
313
|
+
},
|
|
314
|
+
}), 'dsh-messenger-gateway: status')
|
|
315
|
+
|
|
316
|
+
ctx.effect(() => getWebServer().register({
|
|
317
|
+
kind: 'exact', path: '/dsh-messenger-gateway/smoke',
|
|
318
|
+
handler: async (req, res) => {
|
|
319
|
+
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
|
|
320
|
+
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'forbidden' })
|
|
321
|
+
const gw = getGw()
|
|
322
|
+
if (!gw) return writeJson(res, 503, { ok: false, error: 'gateway not running' })
|
|
323
|
+
let payload = {}
|
|
324
|
+
try {
|
|
325
|
+
const raw = (await readBody(req)).toString('utf8').trim()
|
|
326
|
+
if (raw) payload = JSON.parse(raw)
|
|
327
|
+
} catch {
|
|
328
|
+
return writeJson(res, 400, { ok: false, error: 'invalid json' })
|
|
329
|
+
}
|
|
330
|
+
const timeoutMs = Number(payload.timeoutMs) || 10000
|
|
331
|
+
const result = await gw.probeTelegram(timeoutMs)
|
|
332
|
+
writeJson(res, result.ok ? 200 : 502, result)
|
|
333
|
+
},
|
|
334
|
+
}), 'dsh-messenger-gateway: smoke test')
|
|
335
|
+
|
|
336
|
+
ctx.effect(() => getWebServer().register({
|
|
337
|
+
kind: 'exact', path: '/dsh-messenger-gateway/messenger',
|
|
338
|
+
handler: async (req, res) => {
|
|
339
|
+
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET only' })
|
|
340
|
+
writeJson(res, 200, { ok: true, schema: messengerApiSchema })
|
|
341
|
+
},
|
|
342
|
+
}), 'dsh-messenger-gateway: messenger schema')
|
|
343
|
+
|
|
344
|
+
ctx.effect(() => getWebServer().register({
|
|
345
|
+
kind: 'exact', path: '/dsh-messenger-gateway/events',
|
|
346
|
+
handler: async (req, res) => {
|
|
347
|
+
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
|
|
348
|
+
const gw = getGw()
|
|
349
|
+
if (!gw) return writeJson(res, 503, { ok: false, error: 'gateway not ready' })
|
|
350
|
+
let payload
|
|
351
|
+
try {
|
|
352
|
+
payload = JSON.parse((await readBody(req)).toString('utf8') || '{}')
|
|
353
|
+
} catch {
|
|
354
|
+
return writeJson(res, 400, { ok: false, error: 'invalid json' })
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const expectedSecret = source().webhooks?.secret
|
|
358
|
+
if (expectedSecret) {
|
|
359
|
+
const authHeader = req.headers?.authorization || ''
|
|
360
|
+
const bearer = authHeader.startsWith('Bearer ') ? authHeader.slice(7).trim() : ''
|
|
361
|
+
const tokenHeader = req.headers?.['x-webhook-secret'] || ''
|
|
362
|
+
const provided = bearer || tokenHeader || payload.secret
|
|
363
|
+
if (provided !== expectedSecret) {
|
|
364
|
+
return writeJson(res, 401, { ok: false, error: 'unauthorized' })
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const { target, text, home, chatId, threadId, platform = 'telegram', files } = payload
|
|
369
|
+
if (!text && (!files || !files.length)) {
|
|
370
|
+
return writeJson(res, 400, { ok: false, error: 'text or files required' })
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const dest = target || { platform, chatId, threadId, home }
|
|
374
|
+
try {
|
|
375
|
+
const result = await gw.messengerSend(dest, { text, files, replyMarkup: payload.replyMarkup })
|
|
376
|
+
writeJson(res, 200, { ok: true, sent: result })
|
|
377
|
+
} catch (err) {
|
|
378
|
+
writeJson(res, 500, { ok: false, error: err.message })
|
|
379
|
+
}
|
|
380
|
+
},
|
|
381
|
+
}), 'dsh-messenger-gateway: webhook events')
|
|
382
|
+
|
|
383
|
+
ctx.effect(() => getWebServer().register({
|
|
384
|
+
kind: 'exact', path: '/dsh-messenger-gateway/config',
|
|
385
|
+
handler: async (req, res) => {
|
|
386
|
+
if (req.method === 'GET') return writeJson(res, 200, { ok: true, config: publicConfig(source()) })
|
|
387
|
+
if (req.method !== 'PUT') return writeJson(res, 405, { ok: false, error: 'GET or PUT' })
|
|
388
|
+
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'forbidden' })
|
|
389
|
+
if (!settingsApi) return writeJson(res, 503, { ok: false, error: 'settings not ready' })
|
|
390
|
+
let payload
|
|
391
|
+
try { payload = JSON.parse((await readBody(req)).toString('utf8') || '{}') } catch { return writeJson(res, 400, { ok: false, error: 'invalid json' }) }
|
|
392
|
+
if (payload && typeof payload.config === 'object') payload = payload.config
|
|
393
|
+
try {
|
|
394
|
+
const cur = source()
|
|
395
|
+
const nextTg = { ...cur.telegram, ...(payload.telegram || {}) }
|
|
396
|
+
if (payload.telegram && !String(payload.telegram.botToken || '').trim()) nextTg.botToken = cur.telegram.botToken
|
|
397
|
+
if (payload.telegram && Object.prototype.hasOwnProperty.call(payload.telegram, 'webhookSecret') && !String(payload.telegram.webhookSecret || '').trim()) {
|
|
398
|
+
nextTg.webhookSecret = cur.telegram.webhookSecret
|
|
399
|
+
}
|
|
400
|
+
if (payload.telegram?.notifyBridge) {
|
|
401
|
+
nextTg.notifyBridge = { ...cur.telegram.notifyBridge, ...payload.telegram.notifyBridge }
|
|
402
|
+
}
|
|
403
|
+
await settingsApi.replace(Config({
|
|
404
|
+
...cur,
|
|
405
|
+
...payload,
|
|
406
|
+
telegram: nextTg,
|
|
407
|
+
tts: { ...cur.tts, ...(payload.tts || {}) },
|
|
408
|
+
agent: { ...cur.agent, ...(payload.agent || {}) },
|
|
409
|
+
media: { ...cur.media, ...(payload.media || {}) },
|
|
410
|
+
}))
|
|
411
|
+
sync()
|
|
412
|
+
writeJson(res, 200, { ok: true, config: publicConfig(source()) })
|
|
413
|
+
} catch (err) {
|
|
414
|
+
writeJson(res, 400, { ok: false, error: err.message })
|
|
415
|
+
}
|
|
416
|
+
},
|
|
417
|
+
}), 'dsh-messenger-gateway: config')
|
|
418
|
+
|
|
419
|
+
ctx.effect(() => getWebServer().register({
|
|
420
|
+
kind: 'exact', path: '/dsh-messenger-gateway/pairing',
|
|
421
|
+
handler: async (req, res) => {
|
|
422
|
+
if (req.method !== 'GET') return writeJson(res, 405, { ok: false, error: 'GET only' })
|
|
423
|
+
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'forbidden' })
|
|
424
|
+
const gw = getGw()
|
|
425
|
+
if (!gw) return writeJson(res, 503, { ok: false, error: 'gateway not running' })
|
|
426
|
+
writeJson(res, 200, {
|
|
427
|
+
ok: true,
|
|
428
|
+
pending: gw.messenger.pairingPending(),
|
|
429
|
+
approved: gw.messenger.pairingApproved(),
|
|
430
|
+
})
|
|
431
|
+
},
|
|
432
|
+
}), 'dsh-messenger-gateway: pairing list')
|
|
433
|
+
|
|
434
|
+
ctx.effect(() => getWebServer().register({
|
|
435
|
+
kind: 'exact', path: '/dsh-messenger-gateway/pairing/approve',
|
|
436
|
+
handler: async (req, res) => {
|
|
437
|
+
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
|
|
438
|
+
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'forbidden' })
|
|
439
|
+
const gw = getGw()
|
|
440
|
+
if (!gw) return writeJson(res, 503, { ok: false, error: 'gateway not running' })
|
|
441
|
+
let payload
|
|
442
|
+
try { payload = JSON.parse((await readBody(req)).toString('utf8') || '{}') } catch { return writeJson(res, 400, { ok: false, error: 'invalid json' }) }
|
|
443
|
+
const resu = await gw.approvePairingCode(payload.code)
|
|
444
|
+
writeJson(res, resu.ok ? 200 : 400, resu)
|
|
445
|
+
},
|
|
446
|
+
}), 'dsh-messenger-gateway: pairing approve')
|
|
447
|
+
|
|
448
|
+
ctx.effect(() => getWebServer().register({
|
|
449
|
+
kind: 'exact', path: '/dsh-messenger-gateway/pairing/reject',
|
|
450
|
+
handler: async (req, res) => {
|
|
451
|
+
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
|
|
452
|
+
if (!isTrustedSettingsRequest(req)) return writeJson(res, 403, { ok: false, error: 'forbidden' })
|
|
453
|
+
const gw = getGw()
|
|
454
|
+
if (!gw) return writeJson(res, 503, { ok: false, error: 'gateway not running' })
|
|
455
|
+
let payload
|
|
456
|
+
try { payload = JSON.parse((await readBody(req)).toString('utf8') || '{}') } catch { return writeJson(res, 400, { ok: false, error: 'invalid json' }) }
|
|
457
|
+
const resu = gw.rejectPairingCode(payload.code)
|
|
458
|
+
writeJson(res, resu.ok ? 200 : 400, resu)
|
|
459
|
+
},
|
|
460
|
+
}), 'dsh-messenger-gateway: pairing reject')
|
|
461
|
+
|
|
462
|
+
const webhookPath = () => {
|
|
463
|
+
const p = String(source().telegram?.webhookPath || '').trim()
|
|
464
|
+
return p || '/dsh-messenger-gateway/telegram/webhook'
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
ctx.effect(() => getWebServer().register({
|
|
468
|
+
kind: 'exact', path: webhookPath(),
|
|
469
|
+
handler: async (req, res) => {
|
|
470
|
+
if (req.method !== 'POST') return writeJson(res, 405, { ok: false, error: 'POST only' })
|
|
471
|
+
const cfg = source().telegram || {}
|
|
472
|
+
if (cfg.transport !== 'webhook') return writeJson(res, 404, { ok: false, error: 'webhook transport disabled' })
|
|
473
|
+
const secret = String(cfg.webhookSecret || '').trim()
|
|
474
|
+
if (secret) {
|
|
475
|
+
const hdr = String(req.headers['x-telegram-bot-api-secret-token'] || '')
|
|
476
|
+
if (hdr !== secret) return writeJson(res, 403, { ok: false, error: 'bad secret' })
|
|
477
|
+
}
|
|
478
|
+
const gw = getGw()
|
|
479
|
+
const adapter = gw?.getAdapter?.('telegram')
|
|
480
|
+
if (!adapter?.handleWebhookUpdate) return writeJson(res, 503, { ok: false, error: 'telegram adapter unavailable' })
|
|
481
|
+
let update
|
|
482
|
+
try { update = JSON.parse((await readBody(req)).toString('utf8') || '{}') } catch {
|
|
483
|
+
return writeJson(res, 400, { ok: false, error: 'invalid json' })
|
|
484
|
+
}
|
|
485
|
+
try {
|
|
486
|
+
await adapter.handleWebhookUpdate(update)
|
|
487
|
+
writeJson(res, 200, { ok: true })
|
|
488
|
+
} catch (err) {
|
|
489
|
+
writeJson(res, 500, { ok: false, error: err.message })
|
|
490
|
+
}
|
|
491
|
+
},
|
|
492
|
+
}), 'dsh-messenger-gateway: telegram webhook')
|
|
493
|
+
|
|
494
|
+
registerMessengerRoute(ctx, getGw, '/dsh-messenger-gateway/messenger/send', 'send')
|
|
495
|
+
registerMessengerRoute(ctx, getGw, '/dsh-messenger-gateway/messenger/progress', 'progress')
|
|
496
|
+
registerMessengerRoute(ctx, getGw, '/dsh-messenger-gateway/messenger/ask', 'ask')
|
|
497
|
+
|
|
498
|
+
ctx.on('dispose', () => { if (gateway) gateway.stop() })
|
|
499
|
+
sync()
|
|
500
|
+
}
|